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/registry.rb
CHANGED
|
@@ -1,115 +1,35 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require_relative 'models/tool_metadata'
|
|
5
|
+
require_relative 'models/validation_result'
|
|
6
|
+
require_relative 'tool_index'
|
|
7
|
+
require_relative 'schema_validator'
|
|
5
8
|
|
|
6
9
|
module Ukiryu
|
|
7
10
|
# YAML profile registry loader
|
|
8
11
|
#
|
|
9
|
-
#
|
|
12
|
+
# Provides access to tool definitions from YAML profiles in a registry directory.
|
|
13
|
+
# Supports lazy loading: metadata can be loaded without full profile parsing.
|
|
14
|
+
#
|
|
15
|
+
# Features:
|
|
16
|
+
# - Cached version listings to avoid repeated glob operations
|
|
17
|
+
# - Automatic cache invalidation when registry path changes
|
|
10
18
|
class Registry
|
|
11
19
|
class << self
|
|
12
|
-
# Load all tool profiles from a registry directory
|
|
13
|
-
#
|
|
14
|
-
# @param path [String] the registry directory path
|
|
15
|
-
# @param options [Hash] loading options
|
|
16
|
-
# @option options [Boolean] :recursive search recursively (default: true)
|
|
17
|
-
# @option options [Boolean] :validate validate against schema (default: false)
|
|
18
|
-
# @return [Hash] loaded tools keyed by name
|
|
19
|
-
def load_from(path, options = {})
|
|
20
|
-
raise ProfileLoadError, "Registry path not found: #{path}" unless Dir.exist?(path)
|
|
21
|
-
|
|
22
|
-
tools = {}
|
|
23
|
-
recursive = options.fetch(:recursive, true)
|
|
24
|
-
|
|
25
|
-
pattern = recursive ? "**/*.yaml" : "*.yaml"
|
|
26
|
-
files = Dir.glob(File.join(path, "tools", pattern))
|
|
27
|
-
|
|
28
|
-
files.each do |file|
|
|
29
|
-
begin
|
|
30
|
-
profile = load_profile(file)
|
|
31
|
-
name = profile[:name]
|
|
32
|
-
tools[name] ||= []
|
|
33
|
-
tools[name] << profile
|
|
34
|
-
rescue => e
|
|
35
|
-
warn "Warning: Failed to load profile #{file}: #{e.message}"
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
tools
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
# Load a single profile file
|
|
43
|
-
#
|
|
44
|
-
# @param file [String] the path to the YAML file
|
|
45
|
-
# @return [Hash] the loaded profile
|
|
46
|
-
def load_profile(file)
|
|
47
|
-
content = File.read(file)
|
|
48
|
-
profile = YAML.safe_load(content, permitted_classes: [], permitted_symbols: [], aliases: true)
|
|
49
|
-
|
|
50
|
-
# Convert string keys to symbols for consistency
|
|
51
|
-
profile = symbolize_keys(profile)
|
|
52
|
-
|
|
53
|
-
# Resolve inheritance if present
|
|
54
|
-
resolve_inheritance(profile, file)
|
|
55
|
-
|
|
56
|
-
profile
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Load a specific tool by name
|
|
60
|
-
#
|
|
61
|
-
# @param name [String] the tool name
|
|
62
|
-
# @param options [Hash] loading options
|
|
63
|
-
# @option options [String] :version specific version to load
|
|
64
|
-
# @option options [String] :registry_path path to registry
|
|
65
|
-
# @return [Hash, nil] the tool profile or nil if not found
|
|
66
|
-
def load_tool(name, options = {})
|
|
67
|
-
registry_path = options[:registry_path] || @default_registry_path
|
|
68
|
-
|
|
69
|
-
raise ProfileLoadError, "Registry path not configured" unless registry_path
|
|
70
|
-
|
|
71
|
-
# Try version-specific directory first
|
|
72
|
-
version = options[:version]
|
|
73
|
-
if version
|
|
74
|
-
file = File.join(registry_path, "tools", name, "#{version}.yaml")
|
|
75
|
-
return load_profile(file) if File.exist?(file)
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Search in all matching files
|
|
79
|
-
pattern = File.join(registry_path, "tools", name, "*.yaml")
|
|
80
|
-
files = Dir.glob(pattern).sort
|
|
81
|
-
|
|
82
|
-
if files.empty?
|
|
83
|
-
# Try the old format (single file per tool)
|
|
84
|
-
file = File.join(registry_path, "tools", "#{name}.yaml")
|
|
85
|
-
return load_profile(file) if File.exist?(file)
|
|
86
|
-
return nil
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Return the latest version if version not specified
|
|
90
|
-
if version.nil?
|
|
91
|
-
# Sort by version and return the newest
|
|
92
|
-
sorted_files = files.sort_by { |f| Gem::Version.new(File.basename(f, ".yaml")) }
|
|
93
|
-
load_profile(sorted_files.last)
|
|
94
|
-
else
|
|
95
|
-
# Find specific version
|
|
96
|
-
version_file = files.find { |f| File.basename(f, ".yaml") == version }
|
|
97
|
-
version_file ? load_profile(version_file) : nil
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
20
|
# Set the default registry path
|
|
102
21
|
#
|
|
103
22
|
# @param path [String] the default registry path
|
|
104
|
-
def default_registry_path=(path)
|
|
105
|
-
@default_registry_path = path
|
|
106
|
-
end
|
|
107
23
|
|
|
108
24
|
# Get the default registry path
|
|
109
25
|
#
|
|
110
26
|
# @return [String, nil] the default registry path
|
|
111
|
-
|
|
112
|
-
|
|
27
|
+
attr_accessor :default_registry_path
|
|
28
|
+
|
|
29
|
+
# Reset the version cache (mainly for testing)
|
|
30
|
+
def reset_version_cache
|
|
31
|
+
@version_cache = nil
|
|
32
|
+
@registry_cache_key = nil
|
|
113
33
|
end
|
|
114
34
|
|
|
115
35
|
# Get all available tool names
|
|
@@ -119,128 +39,168 @@ module Ukiryu
|
|
|
119
39
|
registry_path = @default_registry_path
|
|
120
40
|
return [] unless registry_path
|
|
121
41
|
|
|
122
|
-
tools_dir = File.join(registry_path,
|
|
42
|
+
tools_dir = File.join(registry_path, 'tools')
|
|
123
43
|
return [] unless Dir.exist?(tools_dir)
|
|
124
44
|
|
|
125
45
|
# List all directories in tools/
|
|
126
|
-
Dir.glob(File.join(tools_dir,
|
|
46
|
+
Dir.glob(File.join(tools_dir, '*')).select do |path|
|
|
127
47
|
File.directory?(path)
|
|
128
48
|
end.map do |path|
|
|
129
49
|
File.basename(path)
|
|
130
50
|
end.sort
|
|
131
51
|
end
|
|
132
52
|
|
|
133
|
-
#
|
|
53
|
+
# Get available versions for a tool (cached)
|
|
134
54
|
#
|
|
135
|
-
# @param
|
|
136
|
-
# @param
|
|
137
|
-
# @
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
55
|
+
# @param name [String, Symbol] the tool name
|
|
56
|
+
# @param registry_path [String, nil] the registry path
|
|
57
|
+
# @return [Hash] mapping of version filename to Gem::Version
|
|
58
|
+
def list_versions(name, registry_path: nil)
|
|
59
|
+
registry_path ||= @default_registry_path
|
|
60
|
+
return {} unless registry_path
|
|
61
|
+
|
|
62
|
+
# Initialize cache
|
|
63
|
+
@version_cache ||= {}
|
|
64
|
+
@registry_cache_key ||= registry_path
|
|
65
|
+
|
|
66
|
+
# Clear cache if registry path changed
|
|
67
|
+
if @registry_cache_key != registry_path
|
|
68
|
+
@version_cache.clear
|
|
69
|
+
@registry_cache_key = registry_path
|
|
70
|
+
end
|
|
148
71
|
|
|
149
|
-
#
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
profile_shells = profile[:shells] || profile[:shell]
|
|
72
|
+
# Check cache
|
|
73
|
+
cache_key = name.to_sym
|
|
74
|
+
return @version_cache[cache_key].dup if @version_cache[cache_key]
|
|
153
75
|
|
|
154
|
-
|
|
155
|
-
|
|
76
|
+
# Build version list
|
|
77
|
+
versions = scan_tool_versions(name, registry_path)
|
|
78
|
+
@version_cache[cache_key] = versions
|
|
156
79
|
|
|
157
|
-
|
|
158
|
-
|
|
80
|
+
versions.dup
|
|
81
|
+
end
|
|
159
82
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
83
|
+
# Load tool metadata only (lightweight, without full parsing)
|
|
84
|
+
# This is much faster than loading the full definition when only metadata is needed
|
|
85
|
+
#
|
|
86
|
+
# Supports both exact name lookup and interface-based discovery
|
|
87
|
+
#
|
|
88
|
+
# @param name [String, Symbol] the tool name or interface name
|
|
89
|
+
# @param options [Hash] loading options
|
|
90
|
+
# @option options [String] :version specific version to load
|
|
91
|
+
# @option options [String] :registry_path path to registry
|
|
92
|
+
# @return [ToolMetadata, nil] the tool metadata or nil if not found
|
|
93
|
+
def load_tool_metadata(name, options = {})
|
|
94
|
+
registry_path = options[:registry_path] || @default_registry_path
|
|
166
95
|
|
|
167
|
-
|
|
168
|
-
|
|
96
|
+
# First try exact name match
|
|
97
|
+
yaml_content = load_tool_yaml(name, options.merge(registry_path: registry_path))
|
|
98
|
+
if yaml_content
|
|
99
|
+
hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol])
|
|
100
|
+
return ToolMetadata.from_hash(hash, tool_name: name.to_s, registry_path: registry_path) if hash
|
|
169
101
|
end
|
|
170
102
|
|
|
171
|
-
#
|
|
172
|
-
|
|
103
|
+
# If not found, try interface-based discovery using ToolIndex
|
|
104
|
+
index = ToolIndex.instance
|
|
105
|
+
index.find_by_interface(name.to_sym)
|
|
173
106
|
end
|
|
174
107
|
|
|
175
|
-
|
|
108
|
+
# Load tool YAML file content (for lutaml-model parsing)
|
|
109
|
+
#
|
|
110
|
+
# @param name [String, Symbol] the tool name
|
|
111
|
+
# @param options [Hash] loading options
|
|
112
|
+
# @option options [String] :version specific version to load
|
|
113
|
+
# @option options [String] :registry_path path to registry
|
|
114
|
+
# @return [String, nil] the YAML content or nil if not found
|
|
115
|
+
def load_tool_yaml(name, options = {})
|
|
116
|
+
registry_path = options[:registry_path] || @default_registry_path
|
|
176
117
|
|
|
177
|
-
# Load all profiles for a specific tool
|
|
178
|
-
def load_tool_profiles(name)
|
|
179
|
-
registry_path = @default_registry_path
|
|
180
118
|
return nil unless registry_path
|
|
181
119
|
|
|
182
|
-
|
|
183
|
-
|
|
120
|
+
# Convert to string for path operations
|
|
121
|
+
name_str = name.to_s
|
|
184
122
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
profile[:_file_path] = file
|
|
191
|
-
profile
|
|
192
|
-
rescue => e
|
|
193
|
-
warn "Warning: Failed to load profile #{file}: #{e.message}"
|
|
194
|
-
[]
|
|
195
|
-
end
|
|
123
|
+
# Try version-specific directory first
|
|
124
|
+
version = options[:version]
|
|
125
|
+
if version
|
|
126
|
+
file = File.join(registry_path, 'tools', name_str, "#{version}.yaml")
|
|
127
|
+
return File.read(file) if File.exist?(file)
|
|
196
128
|
end
|
|
197
|
-
end
|
|
198
129
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
# @param profile [Hash] the profile to resolve
|
|
202
|
-
# @param file_path [String] the path to the profile file
|
|
203
|
-
def resolve_inheritance(profile, file_path)
|
|
204
|
-
return unless profile[:profiles]
|
|
130
|
+
# Use cached version list if available
|
|
131
|
+
versions = list_versions(name_str, registry_path: registry_path)
|
|
205
132
|
|
|
206
|
-
|
|
133
|
+
if versions.empty?
|
|
134
|
+
# Try the old format (single file per tool)
|
|
135
|
+
file = File.join(registry_path, 'tools', "#{name_str}.yaml")
|
|
136
|
+
return File.read(file) if File.exist?(file)
|
|
207
137
|
|
|
208
|
-
|
|
209
|
-
|
|
138
|
+
return nil
|
|
139
|
+
end
|
|
210
140
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
end
|
|
141
|
+
# Return specific version if requested
|
|
142
|
+
if version
|
|
143
|
+
version_file = versions.keys.find { |f| File.basename(f, '.yaml') == version }
|
|
144
|
+
return version_file ? File.read(version_file) : nil
|
|
216
145
|
end
|
|
146
|
+
|
|
147
|
+
# Return the latest version (already sorted from scan_tool_versions)
|
|
148
|
+
File.read(versions.keys.last)
|
|
217
149
|
end
|
|
218
150
|
|
|
219
|
-
#
|
|
220
|
-
|
|
221
|
-
|
|
151
|
+
# Validate a tool profile against the schema
|
|
152
|
+
#
|
|
153
|
+
# @param name [String, Symbol] the tool name
|
|
154
|
+
# @param options [Hash] validation options
|
|
155
|
+
# @option options [String] :version specific version to validate
|
|
156
|
+
# @option options [String] :registry_path path to registry
|
|
157
|
+
# @option options [String] :schema_path path to schema file
|
|
158
|
+
# @return [ValidationResult] the validation result
|
|
159
|
+
def validate_tool(name, options = {})
|
|
160
|
+
yaml_content = load_tool_yaml(name, options)
|
|
161
|
+
return Models::ValidationResult.not_found(name.to_s) unless yaml_content
|
|
162
|
+
|
|
163
|
+
profile = YAML.safe_load(yaml_content, permitted_classes: [Symbol])
|
|
164
|
+
return Models::ValidationResult.invalid(name.to_s, ['Failed to parse YAML']) unless profile
|
|
165
|
+
|
|
166
|
+
errors = SchemaValidator.validate_profile(profile, options)
|
|
167
|
+
if errors.empty?
|
|
168
|
+
Models::ValidationResult.valid(name.to_s)
|
|
169
|
+
else
|
|
170
|
+
Models::ValidationResult.invalid(name.to_s, errors)
|
|
171
|
+
end
|
|
222
172
|
end
|
|
223
173
|
|
|
224
|
-
#
|
|
174
|
+
# Validate all tool profiles in the registry
|
|
225
175
|
#
|
|
226
|
-
# @param
|
|
227
|
-
# @
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
end.transform_values do |value|
|
|
234
|
-
case value
|
|
235
|
-
when Hash
|
|
236
|
-
symbolize_keys(value)
|
|
237
|
-
when Array
|
|
238
|
-
value.map { |v| v.is_a?(Hash) ? symbolize_keys(v) : v }
|
|
239
|
-
else
|
|
240
|
-
value
|
|
241
|
-
end
|
|
176
|
+
# @param options [Hash] validation options
|
|
177
|
+
# @option options [String] :registry_path path to registry
|
|
178
|
+
# @option options [String] :schema_path path to schema file
|
|
179
|
+
# @return [Array<ValidationResult>] list of validation results
|
|
180
|
+
def validate_all_tools(options = {})
|
|
181
|
+
tools.map do |tool_name|
|
|
182
|
+
validate_tool(tool_name, options)
|
|
242
183
|
end
|
|
243
184
|
end
|
|
185
|
+
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
# Scan tool versions and sort by Gem::Version
|
|
189
|
+
#
|
|
190
|
+
# @param name [String] the tool name
|
|
191
|
+
# @param registry_path [String] the registry path
|
|
192
|
+
# @return [Hash] mapping of version filename to Gem::Version
|
|
193
|
+
def scan_tool_versions(name, registry_path)
|
|
194
|
+
pattern = File.join(registry_path, 'tools', name.to_s, '*.yaml')
|
|
195
|
+
files = Dir.glob(pattern)
|
|
196
|
+
|
|
197
|
+
# Sort files by Gem::Version for proper version ordering
|
|
198
|
+
files.sort_by { |f| Gem::Version.new(File.basename(f, '.yaml')) }
|
|
199
|
+
.each_with_object({}) { |file, hash| hash[file] = Gem::Version.new(File.basename(file, '.yaml')) }
|
|
200
|
+
rescue ArgumentError
|
|
201
|
+
# If version parsing fails, return unsorted files
|
|
202
|
+
files.each_with_object({}) { |file, hash| hash[file] = File.basename(file, '.yaml') }
|
|
203
|
+
end
|
|
244
204
|
end
|
|
245
205
|
end
|
|
246
206
|
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ukiryu
|
|
4
|
+
module Response
|
|
5
|
+
# Abstract base class for all response classes
|
|
6
|
+
#
|
|
7
|
+
# This class wraps the raw Executor::Result object and provides
|
|
8
|
+
# a structured interface for accessing command execution results.
|
|
9
|
+
#
|
|
10
|
+
# @abstract
|
|
11
|
+
class Base
|
|
12
|
+
# Create a new response
|
|
13
|
+
#
|
|
14
|
+
# @param result [Executor::Result] the raw execution result
|
|
15
|
+
def initialize(result)
|
|
16
|
+
@result = result
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Check if the command was successful
|
|
20
|
+
#
|
|
21
|
+
# @return [Boolean] true if exit code was 0
|
|
22
|
+
def success?
|
|
23
|
+
@result.status.zero?
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Get the exit code
|
|
27
|
+
#
|
|
28
|
+
# @return [Integer] the exit code
|
|
29
|
+
def exit_code
|
|
30
|
+
@result.status
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Get the exit code meaning (symbol)
|
|
34
|
+
#
|
|
35
|
+
# @return [String, nil] the exit code meaning (e.g., "merge_conflict") or nil if not defined
|
|
36
|
+
def exit_code_meaning
|
|
37
|
+
tool_name = @result.command_info.tool_name
|
|
38
|
+
command_name = @result.command_info.command_name
|
|
39
|
+
return nil unless tool_name && command_name
|
|
40
|
+
|
|
41
|
+
# Look up the tool by name
|
|
42
|
+
require_relative '../tool'
|
|
43
|
+
tool = Tool.find_by(tool_name.to_sym)
|
|
44
|
+
return nil unless tool
|
|
45
|
+
|
|
46
|
+
# Get exit codes from the tool's profile
|
|
47
|
+
profile = tool.profile
|
|
48
|
+
return nil unless profile
|
|
49
|
+
|
|
50
|
+
command_profile = profile.compatible_profile
|
|
51
|
+
return nil unless command_profile
|
|
52
|
+
|
|
53
|
+
# First, try to get exit codes from the specific command
|
|
54
|
+
command = command_profile.command(command_name.to_s)
|
|
55
|
+
exit_codes = command&.exit_codes
|
|
56
|
+
|
|
57
|
+
# Fall back to profile-level exit codes if command doesn't define its own
|
|
58
|
+
exit_codes ||= command_profile.exit_codes
|
|
59
|
+
return nil unless exit_codes
|
|
60
|
+
|
|
61
|
+
exit_codes.meaning(@result.status)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Get the standard output
|
|
65
|
+
#
|
|
66
|
+
# @return [String] the stdout content
|
|
67
|
+
def stdout
|
|
68
|
+
@result.output
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get the standard error
|
|
72
|
+
#
|
|
73
|
+
# @return [String] the stderr content
|
|
74
|
+
def stderr
|
|
75
|
+
@result.error_output
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Get stdout as lines
|
|
79
|
+
#
|
|
80
|
+
# @return [Array<String>] the stdout split into lines
|
|
81
|
+
def stdout_lines
|
|
82
|
+
@result.stdout_lines
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Get stderr as lines
|
|
86
|
+
#
|
|
87
|
+
# @return [Array<String>] the stderr split into lines
|
|
88
|
+
def stderr_lines
|
|
89
|
+
@result.stderr_lines
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Get the command that was executed
|
|
93
|
+
#
|
|
94
|
+
# @return [String] the full command string
|
|
95
|
+
def command
|
|
96
|
+
@result.command_info.full_command
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Get the executable path
|
|
100
|
+
#
|
|
101
|
+
# @return [String] the executable that was run
|
|
102
|
+
def executable
|
|
103
|
+
@result.command_info.executable
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Get the command arguments
|
|
107
|
+
#
|
|
108
|
+
# @return [Array<String>] the arguments passed to the command
|
|
109
|
+
def arguments
|
|
110
|
+
@result.command_info.arguments
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Get the shell type used
|
|
114
|
+
#
|
|
115
|
+
# @return [Symbol] the shell type
|
|
116
|
+
def shell
|
|
117
|
+
@result.command_info.shell
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Get the execution duration
|
|
121
|
+
#
|
|
122
|
+
# @return [Float] duration in seconds
|
|
123
|
+
def duration
|
|
124
|
+
@result.metadata.duration
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Get the formatted duration string
|
|
128
|
+
#
|
|
129
|
+
# @return [String] human-readable duration (e.g., "1.2s", "450ms")
|
|
130
|
+
def formatted_duration
|
|
131
|
+
@result.metadata.formatted_duration
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Get the start time
|
|
135
|
+
#
|
|
136
|
+
# @return [Time] when the command started
|
|
137
|
+
def started_at
|
|
138
|
+
@result.metadata.started_at
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Get the end time
|
|
142
|
+
#
|
|
143
|
+
# @return [Time] when the command finished
|
|
144
|
+
def finished_at
|
|
145
|
+
@result.metadata.finished_at
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Check if the command timed out
|
|
149
|
+
#
|
|
150
|
+
# @return [Boolean] true if the command exceeded its timeout
|
|
151
|
+
def timed_out?
|
|
152
|
+
@result.timeout_exceeded?
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Get the raw result object
|
|
156
|
+
#
|
|
157
|
+
# @return [Executor::Result] the raw execution result
|
|
158
|
+
def raw_result
|
|
159
|
+
@result
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Convert to hash representation
|
|
163
|
+
#
|
|
164
|
+
# @return [Hash] hash representation of the response
|
|
165
|
+
def to_h
|
|
166
|
+
hash = {
|
|
167
|
+
success: success?,
|
|
168
|
+
exit_code: exit_code,
|
|
169
|
+
stdout: stdout,
|
|
170
|
+
stderr: stderr,
|
|
171
|
+
command: command,
|
|
172
|
+
executable: executable,
|
|
173
|
+
arguments: arguments,
|
|
174
|
+
shell: shell,
|
|
175
|
+
duration: duration,
|
|
176
|
+
started_at: started_at.iso8601,
|
|
177
|
+
finished_at: finished_at.iso8601
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
# Add exit code meaning if available
|
|
181
|
+
meaning = exit_code_meaning
|
|
182
|
+
hash[:exit_code_meaning] = meaning if meaning
|
|
183
|
+
|
|
184
|
+
hash
|
|
185
|
+
end
|
|
186
|
+
alias to_hash to_h
|
|
187
|
+
|
|
188
|
+
# String representation
|
|
189
|
+
#
|
|
190
|
+
# @return [String] summary of the response
|
|
191
|
+
def to_s
|
|
192
|
+
if success?
|
|
193
|
+
"Success (exit #{exit_code}#{format_meaning}, #{formatted_duration})"
|
|
194
|
+
else
|
|
195
|
+
"Failed (exit #{exit_code}#{format_meaning}, #{formatted_duration})"
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Format exit code meaning
|
|
200
|
+
#
|
|
201
|
+
# @return [String] formatted meaning (e.g., ": merge_conflict")
|
|
202
|
+
def format_meaning
|
|
203
|
+
meaning = exit_code_meaning
|
|
204
|
+
return '' unless meaning
|
|
205
|
+
|
|
206
|
+
": #{meaning}"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Inspect representation
|
|
210
|
+
#
|
|
211
|
+
# @return [String] detailed inspection string
|
|
212
|
+
def inspect
|
|
213
|
+
"#<#{self.class.name} success=#{success?} exit_code=#{exit_code} duration=#{formatted_duration}>"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|