ukiryu 0.1.6 → 0.2.0

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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/ukiryu/cache.rb +6 -0
  3. data/lib/ukiryu/cache_registry.rb +64 -0
  4. data/lib/ukiryu/cli_commands/base_command.rb +6 -5
  5. data/lib/ukiryu/cli_commands/config_command.rb +7 -10
  6. data/lib/ukiryu/cli_commands/register_command.rb +27 -18
  7. data/lib/ukiryu/cli_commands/validate_command.rb +2 -2
  8. data/lib/ukiryu/command_builder.rb +83 -50
  9. data/lib/ukiryu/config.rb +13 -2
  10. data/lib/ukiryu/debug.rb +20 -9
  11. data/lib/ukiryu/definition/loader.rb +3 -3
  12. data/lib/ukiryu/errors.rb +37 -37
  13. data/lib/ukiryu/executable_locator.rb +40 -16
  14. data/lib/ukiryu/extractors/base_extractor.rb +2 -1
  15. data/lib/ukiryu/extractors/help_parser.rb +3 -0
  16. data/lib/ukiryu/logger.rb +51 -0
  17. data/lib/ukiryu/models/implementation_index.rb +2 -1
  18. data/lib/ukiryu/models/implementation_version.rb +18 -1
  19. data/lib/ukiryu/models/interface.rb +2 -1
  20. data/lib/ukiryu/models/run_environment.rb +0 -2
  21. data/lib/ukiryu/models/semantic_version.rb +174 -0
  22. data/lib/ukiryu/models/stage_metrics.rb +0 -1
  23. data/lib/ukiryu/register.rb +473 -232
  24. data/lib/ukiryu/shell/powershell.rb +209 -89
  25. data/lib/ukiryu/shell/sh.rb +4 -1
  26. data/lib/ukiryu/shell.rb +60 -2
  27. data/lib/ukiryu/tool/command_resolution.rb +2 -1
  28. data/lib/ukiryu/tool/executable_discovery.rb +14 -15
  29. data/lib/ukiryu/tool/loader.rb +543 -0
  30. data/lib/ukiryu/tool/version_detection.rb +1 -3
  31. data/lib/ukiryu/tool.rb +79 -87
  32. data/lib/ukiryu/tool_index.rb +127 -62
  33. data/lib/ukiryu/tools/base.rb +4 -2
  34. data/lib/ukiryu/type.rb +26 -15
  35. data/lib/ukiryu/version.rb +1 -1
  36. data/lib/ukiryu.rb +1 -1
  37. data/spec/fixtures/profiles/ghostscript_10.0.yaml +50 -0
  38. data/spec/fixtures/register/tools/ghostscript/default/10.0.yaml +6 -0
  39. data/spec/spec_helper.rb +10 -6
  40. data/spec/support/tool_helper.rb +2 -0
  41. data/spec/ukiryu/definition/loader_spec.rb +2 -2
  42. data/spec/ukiryu/executor_spec.rb +6 -3
  43. data/spec/ukiryu/models/execution_report_spec.rb +3 -2
  44. data/spec/ukiryu/models/semantic_version_spec.rb +284 -0
  45. data/spec/ukiryu/shell/powershell_integration_spec.rb +165 -0
  46. data/spec/ukiryu/shell/powershell_real_command_spec.rb +143 -0
  47. data/spec/ukiryu/shell/powershell_spec.rb +286 -51
  48. data/spec/ukiryu/tool/loader_spec.rb +148 -0
  49. data/spec/ukiryu/tool_index_spec.rb +110 -18
  50. data/spec/ukiryu/tools/ghostscript_spec.rb +242 -0
  51. data/spec/ukiryu/tools/imagemagick_spec.rb +2 -1
  52. data/spec/ukiryu/tools/inkscape_spec.rb +4 -2
  53. metadata +14 -2
  54. data/lib/ukiryu/register_auto_manager.rb +0 -342
@@ -1,109 +1,153 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'yaml'
4
+ require 'git'
5
+ require 'fileutils'
6
+ require 'pathname'
4
7
  require_relative 'utils'
5
8
  require_relative 'models/interface'
6
9
  require_relative 'models/implementation_index'
7
10
  require_relative 'models/implementation_version'
8
- require_relative 'register_auto_manager'
9
11
 
10
12
  module Ukiryu
11
- # YAML profile register loader
13
+ # Represents a collection of tool definitions
12
14
  #
13
- # Provides access to tool definitions from YAML profiles in a register directory.
14
- # Supports lazy loading: metadata can be loaded without full profile parsing.
15
+ # A Register is a directory containing tool profiles in YAML format.
16
+ # It handles:
17
+ # - Discovery (env variable, dev path, user clone)
18
+ # - Auto-cloning from GitHub
19
+ # - Loading tool definitions
20
+ # - Validation
21
+ #
22
+ # @example Getting the default register
23
+ # register = Ukiryu::Register.default
24
+ # register.tool_names # => ['ghostscript', 'imagemagick', ...]
25
+ #
26
+ # @example Using a specific path
27
+ # register = Ukiryu::Register.at('/path/to/register')
28
+ #
29
+ # @api private - This is an internal class. Developers use Tool.get()
15
30
  #
16
- # Features:
17
- # - Cached version listings to avoid repeated glob operations
18
- # - Automatic cache invalidation when register path changes
19
- # - Automatic register cloning to ~/.ukiryu/register if not configured
20
31
  class Register
32
+ # GitHub repository URL for the register
33
+ REMOTE_URL = 'https://github.com/ukiryu/register'
34
+
35
+ # Default local directory for user clone
36
+ DEFAULT_USER_PATH = '~/.ukiryu/register'
37
+
38
+ # @return [String] the filesystem path to the register
39
+ attr_reader :path
40
+
41
+ # @return [Symbol] how the register was discovered (:env, :dev, :user, :manual)
42
+ attr_reader :source
43
+
44
+ # Error raised when register operations fail
45
+ class Error < StandardError; end
46
+
47
+ # Error raised when register cannot be found or cloned
48
+ class NotFoundError < Error; end
49
+
50
+ # Error raised when cloning fails
51
+ class CloneError < Error; end
52
+
21
53
  class << self
22
- # Set the default register path
54
+ # Get the default register instance
55
+ #
56
+ # Auto-discovers the register using this priority:
57
+ # 1. UKIRYU_REGISTER environment variable
58
+ # 2. Development register (sibling to gem source)
59
+ # 3. User clone at ~/.ukiryu/register (auto-clones if needed)
23
60
  #
24
- # @param path [String] the default register path
61
+ # @return [Register] the default register instance
62
+ def default
63
+ @default ||= discover
64
+ end
25
65
 
26
- # Get the default register path
66
+ # Create a register at a specific path
27
67
  #
28
- # @return [String, nil] the default register path
29
- attr_accessor :default_register_path
68
+ # @param path [String] the filesystem path
69
+ # @return [Register] a new register instance
70
+ def at(path)
71
+ new(File.expand_path(path), source: :manual)
72
+ end
30
73
 
31
- # Reset the version cache (mainly for testing)
32
- def reset_version_cache
33
- @version_cache = nil
34
- @register_cache_key = nil
74
+ # Reset the cached default register (mainly for testing)
75
+ def reset_default
76
+ @default = nil
35
77
  end
36
78
 
37
- # Get all available tool names
38
- # Only returns tools that have an index.yaml (new architecture)
79
+ # Check if a default register exists (without auto-cloning)
39
80
  #
40
- # @return [Array<String>] list of tool names
41
- def tools
42
- register_path = effective_register_path
43
- return [] unless register_path
81
+ # @return [Boolean] true if a register can be found
82
+ def exists?
83
+ path = resolve_path_without_clone
84
+ path && Dir.exist?(path) && valid_structure?(path)
85
+ end
44
86
 
45
- tools_dir = File.join(register_path, 'tools')
46
- return [] unless Dir.exist?(tools_dir)
87
+ # ===== BACKWARD COMPATIBILITY =====
88
+ # These class methods delegate to the default instance for backward compatibility
47
89
 
48
- # List all directories that have an index.yaml file
49
- Dir.glob(File.join(tools_dir, '*', 'index.yaml')).map do |index_file|
50
- File.basename(File.dirname(index_file))
51
- end.sort
90
+ # @deprecated Use Register.default.path instead
91
+ def default_register_path
92
+ default.path
52
93
  end
53
94
 
54
- # Get available versions for a tool (cached) - DEPRECATED, kept for compatibility
55
- #
56
- # @param name [String, Symbol] the tool name
57
- # @param register_path [String, nil] the register path
58
- # @return [Hash] mapping of version filename to version string
95
+ # @deprecated Use ENV['UKIRYU_REGISTER'] = path; Register.reset_default instead
96
+ def default_register_path=(path)
97
+ ENV['UKIRYU_REGISTER'] = path
98
+ reset_default
99
+ end
100
+
101
+ # @deprecated Use Register.reset_default instead
102
+ def reset_version_cache
103
+ reset_default
104
+ end
105
+
106
+ # @deprecated Use Register.default.tool_names instead
107
+ def tools
108
+ default.tool_names
109
+ end
110
+
111
+ # @deprecated Use Register.default.list_versions instead
59
112
  def list_versions(name, register_path: nil)
60
- register_path ||= effective_register_path
61
- return {} unless register_path
62
-
63
- # For new architecture, load the index and get versions
64
- index = load_implementation_index(name, register_path: register_path)
65
- return {} unless index
66
-
67
- # Extract versions from all implementations
68
- # Return a hash mapping version strings to full file paths
69
- versions = {}
70
- index.implementations.each do |impl|
71
- impl_name = impl[:name] || impl['name']
72
- impl_versions = impl[:versions] || impl['versions']
73
- next unless impl_versions
74
-
75
- impl_versions.each do |version_spec|
76
- equals = version_spec[:equals] || version_spec['equals']
77
- file = version_spec[:file] || version_spec['file']
78
- next unless equals && file
79
-
80
- # Build full path: tools/tool_name/implementation_name/file.yaml
81
- full_path = File.join(register_path, 'tools', name.to_s, impl_name.to_s, file)
82
- versions[full_path] = equals
83
- end
84
- end
113
+ register = register_path ? at(register_path) : default
114
+ register.list_versions(name)
115
+ end
85
116
 
86
- versions
117
+ # @deprecated Use Register.default.load_tool_yaml instead
118
+ def load_tool_yaml(name, options = {})
119
+ register = options[:register_path] ? at(options[:register_path]) : default
120
+ register.load_tool_yaml(name, version: options[:version])
87
121
  end
88
122
 
89
- # Load tool metadata only (lightweight, without full parsing)
90
- # This is much faster than loading the full definition when only metadata is needed
91
- #
92
- # Supports both exact name lookup and interface-based discovery
93
- #
94
- # @param name [String, Symbol] the tool name or interface name
95
- # @param options [Hash] loading options
96
- # @option options [String] :version specific version to load
97
- # @option options [String] :register_path path to register
98
- # @return [ToolMetadata, nil] the tool metadata or nil if not found
99
- def load_tool_metadata(name, options = {})
100
- register_path = options[:register_path] || effective_register_path
123
+ # @deprecated Use Register.default.load_implementation_index instead
124
+ def load_implementation_index(tool_name, options = {})
125
+ register = options[:register_path] ? at(options[:register_path]) : default
126
+ register.load_implementation_index(tool_name)
127
+ end
128
+
129
+ # @deprecated Use Register.default.load_implementation_version instead
130
+ def load_implementation_version(tool_name, implementation_name, file_path, options = {})
131
+ register = options[:register_path] ? at(options[:register_path]) : default
132
+ register.load_implementation_version(tool_name, implementation_name, file_path)
133
+ end
101
134
 
135
+ # @deprecated Use Register.default.load_interface instead
136
+ def load_interface(path, options = {})
137
+ register = options[:register_path] ? at(options[:register_path]) : default
138
+ register.load_interface(path)
139
+ end
140
+
141
+ # @deprecated Use Register.default methods instead
142
+ def load_tool_metadata(name, options = {})
102
143
  # First try exact name match
103
- yaml_content = load_tool_yaml(name, options.merge(register_path: register_path))
144
+ yaml_content = load_tool_yaml(name, options)
104
145
  if yaml_content
105
146
  hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true)
106
- return ToolMetadata.from_hash(hash, tool_name: name.to_s, register_path: register_path) if hash
147
+ if hash
148
+ return ToolMetadata.from_hash(hash, tool_name: name.to_s,
149
+ register_path: options[:register_path] || default.path)
150
+ end
107
151
  end
108
152
 
109
153
  # If not found, try interface-based discovery using ToolIndex
@@ -113,8 +157,7 @@ module Ukiryu
113
157
  result = index.find_by_interface(name.to_sym)
114
158
  return result if result
115
159
 
116
- # Try interface name with common version suffix (e.g., imagemagick/1.0)
117
- # This handles tools where the interface is defined with version suffix
160
+ # Try interface name with common version suffix
118
161
  name_str = name.to_s
119
162
  [:"#{name_str}/1.0", :"#{name_str}/1", :"v#{name_str}/1.0"].each do |versioned_interface|
120
163
  result = index.find_by_interface(versioned_interface)
@@ -124,57 +167,7 @@ module Ukiryu
124
167
  nil
125
168
  end
126
169
 
127
- # Load tool YAML file content (for lutaml-model parsing)
128
- #
129
- # @param name [String, Symbol] the tool name
130
- # @param options [Hash] loading options
131
- # @option options [String] :version specific version to load
132
- # @option options [String] :register_path path to register
133
- # @return [String, nil] the YAML content or nil if not found
134
- def load_tool_yaml(name, options = {})
135
- register_path = options[:register_path] || effective_register_path
136
-
137
- return nil unless register_path
138
-
139
- # Convert to string for path operations
140
- name_str = name.to_s
141
-
142
- # Try version-specific directory first
143
- version = options[:version]
144
- if version
145
- file = File.join(register_path, 'tools', name_str, "#{version}.yaml")
146
- return File.read(file) if File.exist?(file)
147
- end
148
-
149
- # Use cached version list if available
150
- versions = list_versions(name_str, register_path: register_path)
151
-
152
- if versions.empty?
153
- # Try the old format (single file per tool)
154
- file = File.join(register_path, 'tools', "#{name_str}.yaml")
155
- return File.read(file) if File.exist?(file)
156
-
157
- return nil
158
- end
159
-
160
- # Return specific version if requested
161
- if version
162
- version_file = versions.keys.find { |f| File.basename(f, '.yaml') == version }
163
- return version_file ? File.read(version_file) : nil
164
- end
165
-
166
- # Return the latest version (already sorted from scan_tool_versions)
167
- File.read(versions.keys.last)
168
- end
169
-
170
- # Validate a tool profile against the schema
171
- #
172
- # @param name [String, Symbol] the tool name
173
- # @param options [Hash] validation options
174
- # @option options [String] :version specific version to validate
175
- # @option options [String] :register_path path to register
176
- # @option options [String] :schema_path path to schema file
177
- # @return [ValidationResult] the validation result
170
+ # @deprecated Use Register.default.validate methods instead
178
171
  def validate_tool(name, options = {})
179
172
  yaml_content = load_tool_yaml(name, options)
180
173
  return Models::ValidationResult.not_found(name.to_s) unless yaml_content
@@ -190,145 +183,393 @@ module Ukiryu
190
183
  end
191
184
  end
192
185
 
193
- # Validate all tool profiles in the register
194
- #
195
- # @param options [Hash] validation options
196
- # @option options [String] :register_path path to register
197
- # @option options [String] :schema_path path to schema file
198
- # @return [Array<ValidationResult>] list of validation results
186
+ # @deprecated Use Register.default.validate methods instead
199
187
  def validate_all_tools(options = {})
200
188
  tools.map do |tool_name|
201
189
  validate_tool(tool_name, options)
202
190
  end
203
191
  end
204
192
 
205
- # Load an Interface by path (e.g., "gzip/1.0")
193
+ private
194
+
195
+ # Discover and create the default register
206
196
  #
207
- # @param path [String] the interface path (e.g., "gzip/1.0")
208
- # @param options [Hash] loading options
209
- # @option options [String] :register_path path to register
210
- # @return [Models::Interface, nil] the interface or nil if not found
211
- def load_interface(path, options = {})
212
- register_path = options[:register_path] || effective_register_path
213
- return nil unless register_path
197
+ # @return [Register] the discovered register
198
+ def discover
199
+ path, source = discover_path
200
+ raise NotFoundError, 'No register found and auto-clone failed' unless path
201
+
202
+ register = new(path, source: source)
203
+ register.ensure_exists!
204
+ register
205
+ end
214
206
 
215
- file = File.join(register_path, 'interfaces', "#{path}.yaml")
216
- return nil unless File.exist?(file)
207
+ # Discover the register path without auto-cloning
208
+ #
209
+ # @return [Array<String, Symbol>, nil] path and source, or nil
210
+ def discover_path
211
+ # 1. Check UKIRYU_REGISTER environment variable
212
+ env_path = ENV['UKIRYU_REGISTER']
213
+ return [env_path, :env] if env_path && Dir.exist?(env_path)
214
+
215
+ # 2. Check development register (sibling to gem source)
216
+ dev_path = calculate_dev_path
217
+ return [dev_path.to_s, :dev] if dev_path&.exist?
218
+
219
+ # 3. Use user clone (may need to be created)
220
+ user_path = File.expand_path(DEFAULT_USER_PATH)
221
+ return [user_path, :user] if Dir.exist?(user_path) && valid_structure?(user_path)
222
+
223
+ # 4. Return user path for auto-clone
224
+ [user_path, :user]
225
+ end
226
+
227
+ # Resolve path without triggering auto-clone
228
+ #
229
+ # @return [String, nil] the path or nil
230
+ def resolve_path_without_clone
231
+ env_path = ENV['UKIRYU_REGISTER']
232
+ return env_path if env_path && Dir.exist?(env_path)
233
+
234
+ dev_path = calculate_dev_path
235
+ return dev_path.to_s if dev_path&.exist?
217
236
 
218
- yaml_content = File.read(file)
219
- hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true)
220
- return nil unless hash
237
+ user_path = File.expand_path(DEFAULT_USER_PATH)
238
+ return user_path if Dir.exist?(user_path) && valid_structure?(user_path)
221
239
 
222
- Models::Interface.from_hash(symbolize_keys(hash))
240
+ nil
223
241
  end
224
242
 
225
- # Load an ImplementationIndex by tool name
243
+ # Calculate the development register path
226
244
  #
227
- # @param tool_name [String, Symbol] the tool name
228
- # @param options [Hash] loading options
229
- # @option options [String] :register_path path to register
230
- # @return [Models::ImplementationIndex, nil] the index or nil if not found
231
- def load_implementation_index(tool_name, options = {})
232
- register_path = options[:register_path] || effective_register_path
245
+ # @return [Pathname, nil] the dev path or nil
246
+ def calculate_dev_path
247
+ this_file = Pathname.new(__FILE__).realpath
248
+ # lib/ukiryu/register.rb -> ../../../register
249
+ this_file.parent.parent.parent.join('register')
250
+ rescue StandardError
251
+ nil
252
+ end
233
253
 
234
- if ENV['UKIRYU_DEBUG_EXECUTABLE']
235
- warn "[UKIRYU DEBUG Register.load_implementation_index] tool_name=#{tool_name}"
236
- warn "[UKIRYU DEBUG] register_path = #{register_path.inspect}"
237
- end
254
+ # Check if a path has valid register structure
255
+ #
256
+ # @param path [String] the path to check
257
+ # @return [Boolean] true if valid
258
+ def valid_structure?(path)
259
+ return false unless path && Dir.exist?(path)
238
260
 
239
- return nil unless register_path
261
+ tools_dir = File.join(path, 'tools')
262
+ return false unless Dir.exist?(tools_dir)
240
263
 
241
- file = File.join(register_path, 'tools', tool_name.to_s, 'index.yaml')
242
- if ENV['UKIRYU_DEBUG_EXECUTABLE']
243
- warn "[UKIRYU DEBUG] index file = #{file.inspect}"
244
- warn "[UKIRYU DEBUG] File exists? #{File.exist?(file)}"
245
- end
246
- return nil unless File.exist?(file)
264
+ # Must have at least one tool definition
265
+ Dir.glob(File.join(tools_dir, '*', '*.yaml')).any?
266
+ end
267
+ end
268
+
269
+ # Initialize a new Register
270
+ #
271
+ # @param path [String] the filesystem path
272
+ # @param source [Symbol] how the register was discovered
273
+ def initialize(path, source: :unknown)
274
+ @path = path
275
+ @source = source
276
+ @version_cache = {}
277
+ end
247
278
 
248
- yaml_content = File.read(file)
249
- hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true)
250
- return nil unless hash
279
+ # Check if the register exists on disk
280
+ #
281
+ # @return [Boolean] true if the register directory exists
282
+ def exists?
283
+ Dir.exist?(path)
284
+ end
251
285
 
252
- # Symbolize string keys recursively
253
- symbolized_hash = symbolize_keys(hash)
254
- Models::ImplementationIndex.from_hash(symbolized_hash)
286
+ # Check if the register has valid structure
287
+ #
288
+ # @return [Boolean] true if valid
289
+ def valid?
290
+ self.class.send(:valid_structure?, path)
291
+ end
292
+
293
+ # Ensure the register exists, cloning if necessary
294
+ #
295
+ # @raise [CloneError] if cloning fails
296
+ def ensure_exists!
297
+ return if exists? && valid?
298
+
299
+ clone!
300
+ end
301
+
302
+ # Clone the register from GitHub
303
+ #
304
+ # @raise [CloneError] if cloning fails
305
+ def clone!
306
+ raise CloneError, "Register already exists at #{path}" if exists? && valid?
307
+
308
+ parent_dir = File.dirname(path)
309
+ FileUtils.mkdir_p(parent_dir) unless Dir.exist?(parent_dir)
310
+
311
+ print "Cloning register from #{REMOTE_URL}..." if $stdout.tty?
312
+
313
+ begin
314
+ Git.clone(REMOTE_URL, path, quiet: true)
315
+ rescue Git::Error => e
316
+ FileUtils.rm_rf(path) if Dir.exist?(path)
317
+ raise CloneError, clone_error_message(e)
255
318
  end
256
319
 
257
- # Load an ImplementationVersion by tool, implementation, and file path
258
- #
259
- # @param tool_name [String, Symbol] the tool name
260
- # @param implementation_name [String, Symbol] the implementation name (e.g., "gnu")
261
- # @param file_path [String] the file path relative to implementation directory
262
- # @param options [Hash] loading options
263
- # @option options [String] :register_path path to register
264
- # @return [Models::ImplementationVersion, nil] the implementation version or nil if not found
265
- def load_implementation_version(tool_name, implementation_name, file_path, options = {})
266
- register_path = options[:register_path] || effective_register_path
320
+ puts 'done' if $stdout.tty?
321
+
322
+ raise CloneError, 'Register clone validation failed' unless valid?
323
+ end
267
324
 
268
- if ENV['UKIRYU_DEBUG_EXECUTABLE']
269
- warn "[UKIRYU DEBUG Register.load_implementation_version] tool=#{tool_name}, impl=#{implementation_name}, file=#{file_path}"
270
- warn "[UKIRYU DEBUG] register_path = #{register_path.inspect}"
325
+ # Update the register (git pull)
326
+ #
327
+ # @raise [Error] if update fails
328
+ def update!
329
+ return clone! unless exists?
330
+
331
+ begin
332
+ null_dev = RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL' : '/dev/null'
333
+ old_redirect = ENV['GIT_REDIRECT_STDERR']
334
+ ENV['GIT_REDIRECT_STDERR'] = null_dev
335
+
336
+ print 'Updating register...' if $stdout.tty?
337
+ git = Git.open(path)
338
+ git.pull
339
+ puts 'done' if $stdout.tty?
340
+ ensure
341
+ if old_redirect
342
+ ENV['GIT_REDIRECT_STDERR'] = old_redirect
343
+ else
344
+ ENV.delete('GIT_REDIRECT_STDERR')
271
345
  end
346
+ end
347
+ rescue Git::Error => e
348
+ raise Error, "Failed to update register: #{e.message}"
349
+ end
272
350
 
273
- return nil unless register_path
351
+ # Get list of all tool names in this register
352
+ #
353
+ # @return [Array<String>] sorted list of tool names
354
+ def tool_names
355
+ tools_dir = File.join(path, 'tools')
356
+ return [] unless Dir.exist?(tools_dir)
274
357
 
275
- file = File.join(register_path, 'tools', tool_name.to_s, implementation_name.to_s, file_path)
276
- if ENV['UKIRYU_DEBUG_EXECUTABLE']
277
- warn "[UKIRYU DEBUG] Loading from file: #{file.inspect}"
278
- warn "[UKIRYU DEBUG] File exists? #{File.exist?(file)}"
279
- end
280
- return nil unless File.exist?(file)
358
+ Dir.glob(File.join(tools_dir, '*', 'index.yaml')).map do |index_file|
359
+ File.basename(File.dirname(index_file))
360
+ end.sort
361
+ end
362
+
363
+ # Check if a tool exists in this register
364
+ #
365
+ # @param name [String, Symbol] the tool name
366
+ # @return [Boolean] true if the tool exists
367
+ def tool?(name)
368
+ index_file = File.join(path, 'tools', name.to_s, 'index.yaml')
369
+ File.exist?(index_file)
370
+ end
371
+
372
+ # Load the implementation index for a tool
373
+ #
374
+ # @param tool_name [String, Symbol] the tool name
375
+ # @return [Models::ImplementationIndex, nil] the index or nil
376
+ def load_implementation_index(tool_name)
377
+ file = File.join(path, 'tools', tool_name.to_s, 'index.yaml')
378
+ return nil unless File.exist?(file)
281
379
 
282
- yaml_content = File.read(file)
283
- hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true)
284
- return nil unless hash
380
+ yaml_content = File.read(file)
381
+ hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true)
382
+ return nil unless hash
383
+
384
+ Models::ImplementationIndex.from_hash(symbolize_keys(hash))
385
+ end
386
+
387
+ # Load a specific implementation version
388
+ #
389
+ # @param tool_name [String, Symbol] the tool name
390
+ # @param impl_name [String, Symbol] the implementation name
391
+ # @param file_path [String] the file path relative to implementation directory
392
+ # @return [Models::ImplementationVersion, nil] the version or nil
393
+ def load_implementation_version(tool_name, impl_name, file_path)
394
+ file = File.join(path, 'tools', tool_name.to_s, impl_name.to_s, file_path)
395
+ return nil unless File.exist?(file)
396
+
397
+ yaml_content = File.read(file)
398
+ hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true)
399
+ return nil unless hash
400
+
401
+ Models::ImplementationVersion.from_hash(symbolize_keys(hash))
402
+ end
285
403
 
286
- Models::ImplementationVersion.from_hash(symbolize_keys(hash))
404
+ # Load raw YAML content for a tool
405
+ #
406
+ # @param name [String, Symbol] the tool name
407
+ # @param version [String, nil] specific version (optional)
408
+ # @return [String, nil] the YAML content or nil
409
+ def load_tool_yaml(name, version: nil)
410
+ name_str = name.to_s
411
+
412
+ # Try version-specific file first
413
+ if version
414
+ file = File.join(path, 'tools', name_str, "#{version}.yaml")
415
+ return File.read(file) if File.exist?(file)
287
416
  end
288
417
 
289
- private
418
+ # Get versions from index
419
+ versions = list_versions(name_str)
420
+ return load_legacy_tool_yaml(name_str) if versions.empty?
290
421
 
291
- # Get the effective register path
292
- #
293
- # Returns the manually set path if available, otherwise uses
294
- # RegisterAutoManager to get or create the default path.
295
- #
296
- # @return [String, nil] the register path, or nil if unavailable
297
- def effective_register_path
298
- # If manually set, use that
299
- return @default_register_path if @default_register_path
300
-
301
- # Otherwise, use RegisterAutoManager (auto-clone if needed)
302
- # Use :: to reference top-level Ukiryu namespace
303
- auto_path = ::Ukiryu::RegisterAutoManager.register_path
304
- warn "[UKIRYU DEBUG] Using RegisterAutoManager path: #{auto_path.inspect}" if ENV['UKIRYU_DEBUG_EXECUTABLE']
305
- auto_path
422
+ # Return specific version or latest
423
+ if version
424
+ version_file = versions.keys.find { |f| File.basename(f, '.yaml') == version }
425
+ return version_file ? File.read(version_file) : nil
306
426
  end
307
427
 
308
- # Recursively symbolize hash keys
309
- #
310
- # @param hash [Hash] the hash to symbolize
311
- # @return [Hash] hash with symbolized keys
312
- def symbolize_keys(hash)
313
- Utils.symbolize_keys(hash)
428
+ File.read(versions.keys.last)
429
+ end
430
+
431
+ # Load an interface definition
432
+ #
433
+ # @param interface_path [String] the interface path (e.g., "gzip/1.0")
434
+ # @return [Models::Interface, nil] the interface or nil
435
+ def load_interface(interface_path)
436
+ file = File.join(path, 'interfaces', "#{interface_path}.yaml")
437
+ return nil unless File.exist?(file)
438
+
439
+ yaml_content = File.read(file)
440
+ hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true)
441
+ return nil unless hash
442
+
443
+ Models::Interface.from_hash(symbolize_keys(hash))
444
+ end
445
+
446
+ # List available versions for a tool
447
+ #
448
+ # @param tool_name [String, Symbol] the tool name
449
+ # @return [Hash] mapping of full file path to version string
450
+ def list_versions(tool_name)
451
+ index = load_implementation_index(tool_name)
452
+ return {} unless index
453
+
454
+ versions = {}
455
+ index.implementations.each do |impl|
456
+ impl_name = impl[:name] || impl['name']
457
+ impl_versions = impl[:versions] || impl['versions']
458
+ next unless impl_versions
459
+
460
+ impl_versions.each do |version_spec|
461
+ equals = version_spec[:equals] || version_spec['equals']
462
+ file = version_spec[:file] || version_spec['file']
463
+ next unless equals && file
464
+
465
+ full_path = File.join(path, 'tools', tool_name.to_s, impl_name.to_s, file)
466
+ versions[full_path] = equals
467
+ end
314
468
  end
315
469
 
316
- # Scan tool versions and sort by Gem::Version
317
- #
318
- # @param name [String] the tool name
319
- # @param register_path [String] the register path
320
- # @return [Hash] mapping of version filename to Gem::Version
321
- def scan_tool_versions(name, register_path)
322
- pattern = File.join(register_path, 'tools', name.to_s, '*.yaml')
323
- files = Dir.glob(pattern)
324
-
325
- # Sort files by Gem::Version for proper version ordering
326
- files.sort_by { |f| Gem::Version.new(File.basename(f, '.yaml')) }
327
- .each_with_object({}) { |file, hash| hash[file] = Gem::Version.new(File.basename(file, '.yaml')) }
328
- rescue ArgumentError
329
- # If version parsing fails, return unsorted files
330
- files.each_with_object({}) { |file, hash| hash[file] = File.basename(file, '.yaml') }
470
+ versions
471
+ end
472
+
473
+ # Get information about this register
474
+ #
475
+ # @return [Hash] register information
476
+ def info
477
+ {
478
+ path: path,
479
+ source: source,
480
+ exists: exists?,
481
+ valid: valid?,
482
+ tools_count: tool_names.count,
483
+ git_info: git_info
484
+ }
485
+ end
486
+
487
+ private
488
+
489
+ # Load legacy single-file tool YAML
490
+ #
491
+ # @param name [String] the tool name
492
+ # @return [String, nil] the YAML content or nil
493
+ def load_legacy_tool_yaml(name)
494
+ file = File.join(path, 'tools', "#{name}.yaml")
495
+ File.read(file) if File.exist?(file)
496
+ end
497
+
498
+ # Get git information for this register
499
+ #
500
+ # @return [Hash, nil] git info or nil
501
+ def git_info
502
+ git_dir = File.join(path, '.git')
503
+ return nil unless Dir.exist?(git_dir)
504
+
505
+ null_dev = RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL' : '/dev/null'
506
+ old_redirect = ENV['GIT_REDIRECT_STDERR']
507
+ ENV['GIT_REDIRECT_STDERR'] = null_dev
508
+
509
+ git = Git.open(path)
510
+ log = git.log(1).to_a
511
+
512
+ {
513
+ branch: git.current_branch,
514
+ commit: log.first&.sha&.[](0..7),
515
+ last_update: log.first&.date && Time.at(log.first.date.to_i)
516
+ }
517
+ rescue Git::Error, IOError, Errno::ENOENT
518
+ nil
519
+ ensure
520
+ if old_redirect
521
+ ENV['GIT_REDIRECT_STDERR'] = old_redirect
522
+ else
523
+ ENV.delete('GIT_REDIRECT_STDERR')
524
+ end
525
+ end
526
+
527
+ # Generate a helpful error message for clone failures
528
+ #
529
+ # @param error [Git::Error] the error
530
+ # @return [String] the error message
531
+ def clone_error_message(error)
532
+ msg = error.message.to_s
533
+
534
+ if msg.include?('cannot find') || msg.include?('not found') || msg.include?('path specified')
535
+ <<~ERROR
536
+ Failed to clone register: #{msg}
537
+
538
+ This error usually means git is not in PATH or the target directory is not accessible.
539
+
540
+ To fix this:
541
+ 1. Verify git is installed and in PATH: git --version
542
+ 2. On Windows, ensure Git for Windows is installed from https://git-scm.com
543
+ 3. Or set UKIRYU_REGISTER to use a local register path
544
+
545
+ Example (Windows):
546
+ set UKIRYU_REGISTER=C:\\path\\to\\register
547
+
548
+ Example (Unix):
549
+ export UKIRYU_REGISTER=/path/to/register
550
+ ERROR
551
+ else
552
+ <<~ERROR
553
+ Failed to clone register from #{REMOTE_URL}: #{msg}
554
+
555
+ To fix this:
556
+ 1. Check your internet connection
557
+ 2. Verify git is installed: git --version
558
+ 3. Manually clone: git clone #{REMOTE_URL} #{path}
559
+ 4. Or set UKIRYU_REGISTER to use a local register path
560
+
561
+ Example:
562
+ export UKIRYU_REGISTER=/path/to/register
563
+ ERROR
331
564
  end
332
565
  end
566
+
567
+ # Recursively symbolize hash keys
568
+ #
569
+ # @param hash [Hash] the hash to symbolize
570
+ # @return [Hash] hash with symbolized keys
571
+ def symbolize_keys(hash)
572
+ Utils.symbolize_keys(hash)
573
+ end
333
574
  end
334
575
  end