ukiryu 0.1.7 → 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 +188 -99
  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 +228 -60
  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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1cb7b872cb1bf64055841e0a8d278b1b57f97fe7bf2133a52067eeee8d51f9f
4
- data.tar.gz: 200175ef0a1a88d193396363b8a6335a01ed8d3a91863d660b25767f86abab4a
3
+ metadata.gz: '09496e921523ef4d99248688fee0293e542ff1cbfc7759403e062822d843f9ba'
4
+ data.tar.gz: 3e094dc9c7af3313d3d3e5d6179a491b037ce643ecf79d00715c5c6099f7b3e2
5
5
  SHA512:
6
- metadata.gz: b3a7ed29941fab82a1b5b055a246234245ae7ba7c50d6d779b107b5a64d567bdbc4a71b3480182aab57831ab0a3119c36e71fa273157ace8b6f316c6ea11467b
7
- data.tar.gz: 03f7e09b741cb05f4a0188511326438671d48a54444441d8abb2d15dfb969afd2ca6eb435dc1b9f0c893bb5015cf8c44b5131cf778c2e8d06062d6ba22e3aafc
6
+ metadata.gz: d357286b89e3edaa8a495f965146effd6ffe06250f8f378c1e38ca6eef8d349abbe1698648e986c6bde2be52ff57637433ef6360175172e171d9db6155856fc4
7
+ data.tar.gz: b3cd0be008011acc0f659b77b651163b61af823e63af3e68486acdc82beb82d074b6a0cb84a21ea299c813e1c201d3089123dc83e687a3366f12ce3cd10318b6
data/lib/ukiryu/cache.rb CHANGED
@@ -50,6 +50,12 @@ module Ukiryu
50
50
  @mutex = Mutex.new if thread_safe
51
51
  end
52
52
 
53
+ # @return [Integer] maximum cache size
54
+ attr_reader :max_size
55
+
56
+ # @return [Integer, nil] time-to-live in seconds
57
+ attr_reader :ttl
58
+
53
59
  # Get a value from the cache
54
60
  #
55
61
  # @param key [Object] the cache key
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ukiryu
4
+ # Centralized cache registry for managing all Ukiryu caches.
5
+ #
6
+ # Provides a single point of control for:
7
+ # - Clearing all caches (useful for testing)
8
+ # - Getting cache statistics (useful for debugging)
9
+ # - Accessing individual caches by name
10
+ #
11
+ # @example Clear all caches
12
+ # Ukiryu::CacheRegistry.clear_all
13
+ #
14
+ # @example Get cache statistics
15
+ # Ukiryu::CacheRegistry.stats
16
+ # # => { tool_cache: { size: 10, hits: 100, misses: 5 }, ... }
17
+ #
18
+ module CacheRegistry
19
+ class << self
20
+ # Get all registered caches
21
+ #
22
+ # @return [Array<Cache>] list of cache instances
23
+ def caches
24
+ [
25
+ ToolCache.cache,
26
+ Definition::Loader.profile_cache
27
+ ].compact
28
+ end
29
+
30
+ # Clear all registered caches
31
+ #
32
+ # @return [void]
33
+ def clear_all
34
+ caches.each(&:clear)
35
+ nil
36
+ end
37
+
38
+ # Get statistics for all caches
39
+ #
40
+ # @return [Hash] cache name => stats hash
41
+ def stats
42
+ {
43
+ tool_cache: cache_stats(ToolCache.cache),
44
+ definition_cache: cache_stats(Definition::Loader.profile_cache)
45
+ }.compact
46
+ end
47
+
48
+ private
49
+
50
+ # Get stats for a single cache
51
+ #
52
+ # @param cache [Cache, nil] the cache instance
53
+ # @return [Hash, nil] stats hash or nil if cache is nil
54
+ def cache_stats(cache)
55
+ return nil unless cache
56
+
57
+ {
58
+ size: cache.size,
59
+ max_size: cache.max_size
60
+ }
61
+ end
62
+ end
63
+ end
64
+ end
@@ -36,7 +36,9 @@ module Ukiryu
36
36
  register_path = custom_path || config.register || default_register_path
37
37
  return unless register_path && Dir.exist?(register_path)
38
38
 
39
- Ukiryu::Register.default_register_path = register_path
39
+ # Set UKIRYU_REGISTER env and reset the default register
40
+ ENV['UKIRYU_REGISTER'] = register_path
41
+ Ukiryu::Register.reset_default
40
42
  end
41
43
 
42
44
  # Apply CLI options to the Config instance
@@ -80,11 +82,10 @@ module Ukiryu
80
82
 
81
83
  # Get the default register path
82
84
  #
83
- # @return [String, nil] the default register path from RegisterAutoManager
85
+ # @return [String, nil] the default register path
84
86
  def default_register_path
85
- # Use RegisterAutoManager to find or create the register
86
- Ukiryu::RegisterAutoManager.register_path
87
- rescue StandardError
87
+ Ukiryu::Register.default.path
88
+ rescue Ukiryu::Register::Error
88
89
  nil
89
90
  end
90
91
 
@@ -222,23 +222,20 @@ module Ukiryu
222
222
  #
223
223
  # @return [String] formatted register display
224
224
  def format_register_display
225
- info = Ukiryu::RegisterAutoManager.register_info
225
+ register = Ukiryu::Register.default
226
+ info = register.info
226
227
 
227
- case info[:status]
228
- when :ok
229
- # Register exists and is valid
228
+ if info[:valid]
230
229
  source_label = format_source_label(info[:source])
231
230
  tools_count = info[:tools_count] ? " (#{info[:tools_count]} tools)" : ''
232
231
  "#{info[:path]} [#{source_label}]#{tools_count}"
233
- when :not_cloned, :not_found
234
- # Register not cloned yet
235
- '~/.ukiryu/register (not found - run: ukiryu register update)'
236
- when :invalid
237
- # Register exists but is invalid
232
+ elsif info[:exists]
238
233
  "#{info[:path]} (invalid - run: ukiryu register update --force)"
239
234
  else
240
- '(unknown)'
235
+ '~/.ukiryu/register (not found - run: ukiryu register update)'
241
236
  end
237
+ rescue Ukiryu::Register::Error
238
+ '~/.ukiryu/register (not found - run: ukiryu register update)'
242
239
  end
243
240
 
244
241
  # Format source label for display
@@ -21,7 +21,7 @@ module Ukiryu
21
21
  else
22
22
  error!("Unknown subcommand: #{subcommand}. Valid subcommands: info, update, path")
23
23
  end
24
- rescue Ukiryu::RegisterAutoManager::RegisterError => e
24
+ rescue Ukiryu::Register::Error => e
25
25
  error!("Register error: #{e.message}")
26
26
  end
27
27
 
@@ -31,21 +31,16 @@ module Ukiryu
31
31
  #
32
32
  # @param options [Hash] command options
33
33
  def show_info(_options = {})
34
- info = Ukiryu::RegisterAutoManager.register_info
34
+ info = Ukiryu::Register.default.info
35
35
 
36
36
  say 'Register Information', :cyan
37
37
  say ''
38
38
 
39
- case info[:status]
39
+ case determine_status(info)
40
40
  when :not_found
41
41
  say ' Status: Not configured', :red
42
42
  say ''
43
43
  say ' No register found. Run: ukiryu register update', :yellow
44
- when :not_cloned
45
- say ' Status: Not cloned', :yellow
46
- say " Expected path: #{info[:path]}", :dim
47
- say ''
48
- say ' Run: ukiryu register update', :yellow
49
44
  when :invalid
50
45
  say ' Status: Invalid', :red
51
46
  say " Path: #{info[:path]}", :dim
@@ -58,11 +53,14 @@ module Ukiryu
58
53
 
59
54
  say " Tools available: #{info[:tools_count]}", :dim if info[:tools_count]
60
55
 
61
- say " Branch: #{info[:branch]}", :dim if info[:branch]
62
-
63
- say " Commit: #{info[:commit]}", :dim if info[:commit]
64
-
65
- say " Last updated: #{info[:last_update].strftime('%Y-%m-%d %H:%M:%S')}", :dim if info[:last_update]
56
+ if info[:git_info]
57
+ say " Branch: #{info[:git_info][:branch]}", :dim if info[:git_info][:branch]
58
+ say " Commit: #{info[:git_info][:commit]}", :dim if info[:git_info][:commit]
59
+ if info[:git_info][:last_update]
60
+ say " Last updated: #{info[:git_info][:last_update].strftime('%Y-%m-%d %H:%M:%S')}",
61
+ :dim
62
+ end
63
+ end
66
64
  end
67
65
 
68
66
  say ''
@@ -74,7 +72,15 @@ module Ukiryu
74
72
  say ' UKIRYU_REGISTER (not set)', :dim
75
73
  end
76
74
 
77
- show_manual_setup_help if info[:status] != :ok
75
+ show_manual_setup_help unless info[:valid]
76
+ end
77
+
78
+ # Determine register status from info hash
79
+ def determine_status(info)
80
+ return :not_found unless info[:exists]
81
+ return :invalid unless info[:valid]
82
+
83
+ :ok
78
84
  end
79
85
 
80
86
  # Update the register
@@ -85,11 +91,14 @@ module Ukiryu
85
91
 
86
92
  if force
87
93
  say 'Force re-cloning register...', :yellow
94
+ FileUtils.rm_rf(Ukiryu::Register.default.path) if Dir.exist?(Ukiryu::Register.default.path)
95
+ Ukiryu::Register.reset_default
88
96
  else
89
97
  say 'Updating register...', :cyan
90
98
  end
91
99
 
92
- Ukiryu::RegisterAutoManager.update_register(force: force)
100
+ register = Ukiryu::Register.default
101
+ register.update!
93
102
 
94
103
  say 'Register updated successfully!', :green
95
104
  show_info(options)
@@ -97,9 +106,9 @@ module Ukiryu
97
106
 
98
107
  # Show the register path
99
108
  def show_path
100
- path = Ukiryu::RegisterAutoManager.register_path
109
+ path = Ukiryu::Register.default.path
101
110
 
102
- if path
111
+ if path && Dir.exist?(path)
103
112
  say path
104
113
  else
105
114
  error!('Register not available. Run: ukiryu register update')
@@ -129,7 +138,7 @@ module Ukiryu
129
138
  say 'Manual setup:', :cyan
130
139
  say ''
131
140
  say ' 1. Clone the register:'
132
- say " git clone #{Ukiryu::RegisterAutoManager::REGISTER_URL} ~/.ukiryu/register"
141
+ say ' git clone https://github.com/ukiryu/register ~/.ukiryu/register'
133
142
  say ''
134
143
  say ' 2. Or set environment variable:'
135
144
  say ' export UKIRYU_REGISTER=/path/to/register'
@@ -163,7 +163,7 @@ module Ukiryu
163
163
  #
164
164
  # This method runs executable validation against all tool definitions
165
165
  def test_all_executables
166
- register_path = Ukiryu::RegisterAutoManager.register_path
166
+ register_path = Ukiryu::Register.default.path
167
167
  return say_error("Register not found: #{register_path}") unless Dir.exist?(register_path)
168
168
 
169
169
  tools_dir = File.join(register_path, 'tools')
@@ -453,7 +453,7 @@ module Ukiryu
453
453
 
454
454
  # Validate all definitions in register
455
455
  def validate_all
456
- register_path = Ukiryu::RegisterAutoManager.register_path
456
+ register_path = Ukiryu::Register.default.path
457
457
  return say_error("Register not found: #{register_path}") unless Dir.exist?(register_path)
458
458
 
459
459
  tools_dir = File.join(register_path, 'tools')
@@ -17,11 +17,10 @@ module Ukiryu
17
17
  def build_args(command, params)
18
18
  args = []
19
19
 
20
- # Debug logging for Ruby 4.0 CI - log all params
21
- if ENV['UKIRYU_DEBUG_EXECUTABLE']
22
- warn "[UKIRYU DEBUG CommandBuilder#build_args] params: #{params.inspect}"
23
- warn "[UKIRYU DEBUG CommandBuilder#build_args] params.class: #{params.class}"
24
- end
20
+ # Debug logging for CI - log all params
21
+ Logger.debug("command: #{command.name}", category: :executable)
22
+ Logger.debug("params: #{params.inspect}", category: :executable)
23
+ Logger.debug("params.class: #{params.class}", category: :executable)
25
24
 
26
25
  # Add subcommand prefix if present (e.g., for ImageMagick "magick convert")
27
26
  args << command.subcommand if command.subcommand
@@ -45,6 +44,11 @@ module Ukiryu
45
44
  next if params[param_key].nil?
46
45
 
47
46
  formatted_opt = format_option(opt_def, params[param_key])
47
+
48
+ # Debug logging
49
+ Logger.debug("formatted_opt for #{param_key}: #{formatted_opt.inspect}",
50
+ category: :executable)
51
+
48
52
  Array(formatted_opt).each { |opt| args << opt unless opt.nil? || opt.empty? }
49
53
  end
50
54
 
@@ -65,12 +69,11 @@ module Ukiryu
65
69
  last_arg = arguments.find(&:last?)
66
70
  regular_args = arguments.reject(&:last?)
67
71
 
68
- # Debug logging for Ruby 4.0 CI - log arguments
69
- if ENV['UKIRYU_DEBUG_EXECUTABLE']
70
- warn "[UKIRYU DEBUG CommandBuilder#build_args] arguments: #{arguments.inspect}"
71
- warn "[UKIRYU DEBUG CommandBuilder#build_args] regular_args: #{regular_args.map(&:name_sym).inspect}"
72
- warn "[UKIRYU DEBUG CommandBuilder#build_args] last_arg: #{last_arg&.name_sym.inspect}"
73
- end
72
+ # Debug logging for arguments
73
+ Logger.debug("arguments: #{arguments.inspect}", category: :executable)
74
+ Logger.debug("regular_args: #{regular_args.map(&:name_sym).inspect}",
75
+ category: :executable)
76
+ Logger.debug("last_arg: #{last_arg&.name_sym.inspect}", category: :executable)
74
77
 
75
78
  # Add regular positional arguments (in order, excluding "last")
76
79
  regular_args.sort_by(&:numeric_position).each do |arg_def|
@@ -80,21 +83,17 @@ module Ukiryu
80
83
  value = params[param_key]
81
84
  next if value.nil?
82
85
 
83
- # Debug logging for Ruby 4.0 CI
84
- if ENV['UKIRYU_DEBUG_EXECUTABLE']
85
- warn "[UKIRYU DEBUG CommandBuilder#build_args] param_key: #{param_key.inspect}"
86
- warn "[UKIRYU DEBUG CommandBuilder#build_args] value.class: #{value.class}"
87
- warn "[UKIRYU DEBUG CommandBuilder#build_args] value.inspect: #{value.inspect}"
88
- warn "[UKIRYU DEBUG CommandBuilder#build_args] arg_def.variadic: #{arg_def.variadic}"
89
- end
86
+ # Debug logging
87
+ Logger.debug("param_key: #{param_key.inspect}", category: :executable)
88
+ Logger.debug("value.class: #{value.class}", category: :executable)
89
+ Logger.debug("value.inspect: #{value.inspect}", category: :executable)
90
+ Logger.debug("arg_def.variadic: #{arg_def.variadic}", category: :executable)
90
91
 
91
92
  if arg_def.variadic
92
93
  # Variadic argument - expand array
93
94
  array = Ukiryu::Type.validate(value, :array, arg_def)
94
- if ENV['UKIRYU_DEBUG_EXECUTABLE']
95
- warn "[UKIRYU DEBUG CommandBuilder#build_args] array.class: #{array.class}"
96
- warn "[UKIRYU DEBUG CommandBuilder#build_args] array.inspect: #{array.inspect}"
97
- end
95
+ Logger.debug("array.class: #{array.class}", category: :executable)
96
+ Logger.debug("array.inspect: #{array.inspect}", category: :executable)
98
97
  array.each { |v| args << format_arg(v, arg_def) }
99
98
  else
100
99
  args << format_arg(value, arg_def)
@@ -124,11 +123,12 @@ module Ukiryu
124
123
  end
125
124
  end
126
125
 
127
- # Debug logging for Ruby 4.0 CI
128
- if ENV['UKIRYU_DEBUG_EXECUTABLE']
129
- warn "[UKIRYU DEBUG CommandBuilder#build_args] Built args: #{args.inspect}"
130
- warn "[UKIRYU DEBUG CommandBuilder#build_args] Args class: #{args.class}"
131
- warn "[UKIRYU DEBUG CommandBuilder#build_args] Args size: #{args.size}"
126
+ # Debug logging for final args
127
+ Logger.debug("Final args: #{args.inspect}", category: :executable)
128
+ Logger.debug("Args class: #{args.class}", category: :executable)
129
+ Logger.debug("Args size: #{args.size}", category: :executable)
130
+ args.each_with_index do |arg, i|
131
+ Logger.debug("args[#{i}]: #{arg.inspect} (#{arg.class})", category: :executable)
132
132
  end
133
133
 
134
134
  args
@@ -144,7 +144,7 @@ module Ukiryu
144
144
  Ukiryu::Type.validate(value, arg_def.type || :string, arg_def)
145
145
 
146
146
  # Apply platform-specific path formatting
147
- if arg_def.type == :file
147
+ if arg_def.type.to_s == 'file'
148
148
  shell = Ukiryu::Shell::InstanceCache.instance_for(@shell)
149
149
  shell.format_path(value.to_s)
150
150
  else
@@ -161,6 +161,13 @@ module Ukiryu
161
161
  # Validate type
162
162
  Ukiryu::Type.validate(value, opt_def.type || :string, opt_def)
163
163
 
164
+ # Debug logging - trace the full option formatting
165
+ Logger.debug("opt_def.name: #{opt_def.name.inspect}", category: :executable)
166
+ Logger.debug("opt_def.cli: #{opt_def.cli.inspect}", category: :executable)
167
+ Logger.debug("opt_def.assignment_delimiter: #{opt_def.assignment_delimiter.inspect}",
168
+ category: :executable)
169
+ Logger.debug("value: #{value.inspect} (#{value.class})", category: :executable)
170
+
164
171
  # Handle boolean types - just return the CLI flag (no value)
165
172
  type_val = opt_def.type
166
173
  if [:boolean, TrueClass, 'boolean'].include?(type_val)
@@ -173,27 +180,55 @@ module Ukiryu
173
180
  delimiter_sym = opt_def.assignment_delimiter_sym
174
181
  separator = opt_def.separator || '='
175
182
 
183
+ Logger.debug("cli variable: #{cli.inspect}", category: :executable)
184
+ Logger.debug("delimiter_sym: #{delimiter_sym.inspect}", category: :executable)
185
+
176
186
  # Auto-detect delimiter based on CLI prefix
177
187
  delimiter_sym = detect_delimiter(cli) if delimiter_sym == :auto
178
188
 
179
- # Convert value to string (handle symbols)
180
- value_str = value.is_a?(Symbol) ? value.to_s : value.to_s
189
+ Logger.debug("delimiter_sym after detect: #{delimiter_sym.inspect}",
190
+ category: :executable)
191
+
192
+ # Convert value to string (handle symbols and file paths)
193
+ if value.is_a?(Symbol)
194
+ value_str = value.to_s
195
+ elsif opt_def.type.to_s == 'file'
196
+ # Apply platform-specific path formatting for file types
197
+ shell_instance = Ukiryu::Shell::InstanceCache.instance_for(@shell)
198
+ Logger.debug("FILE type detected: opt_def.name=#{opt_def.name}, value=#{value.inspect}",
199
+ category: :executable)
200
+ Logger.debug("@shell=#{@shell.inspect}, shell_instance=#{shell_instance.class}",
201
+ category: :executable)
202
+ Logger.debug("Platform.windows?=#{Ukiryu::Platform.windows? if defined?(Ukiryu::Platform)}",
203
+ category: :executable)
204
+ value_str = shell_instance.format_path(value.to_s)
205
+ Logger.debug("format_path result: #{value_str.inspect}", category: :executable)
206
+ else
207
+ value_str = value.to_s
208
+ end
181
209
 
182
210
  # Handle array values with separator
183
211
  if value.is_a?(Array) && separator
184
- joined = value.join(separator)
185
- case delimiter_sym
186
- when :equals
187
- "#{cli}=#{joined}"
188
- when :space
189
- [cli, joined] # Return array for space-separated
190
- when :colon
191
- "#{cli}:#{joined}"
192
- when :none
193
- cli
194
- else
195
- "#{cli}=#{joined}"
196
- end
212
+ # Apply path formatting to each element if type is file
213
+ formatted_values = if opt_def.type.to_s == 'file'
214
+ shell_instance ||= Ukiryu::Shell::InstanceCache.instance_for(@shell)
215
+ value.map { |v| shell_instance.format_path(v.to_s) }
216
+ else
217
+ value.map(&:to_s)
218
+ end
219
+ joined = formatted_values.join(separator)
220
+ result = case delimiter_sym
221
+ when :equals
222
+ "#{cli}=#{joined}"
223
+ when :space
224
+ [cli, joined] # Return array for space-separated
225
+ when :colon
226
+ "#{cli}:#{joined}"
227
+ when :none
228
+ cli
229
+ else
230
+ "#{cli}=#{joined}"
231
+ end
197
232
  else
198
233
  result = case delimiter_sym
199
234
  when :equals
@@ -207,15 +242,13 @@ module Ukiryu
207
242
  else
208
243
  "#{cli}=#{value_str}"
209
244
  end
245
+ end
210
246
 
211
- # Debug logging for Ruby 3.4+ CI
212
- if ENV['UKIRYU_DEBUG_EXECUTABLE']
213
- warn "[UKIRYU DEBUG format_option] result: #{result.inspect}"
214
- warn "[UKIRYU DEBUG format_option] result.class: #{result.class}"
215
- end
247
+ # Debug logging for result
248
+ Logger.debug("FINAL result: #{result.inspect}", category: :executable)
249
+ Logger.debug("result.class: #{result.class}", category: :executable)
216
250
 
217
- result
218
- end
251
+ result
219
252
  end
220
253
 
221
254
  # Detect assignment delimiter based on CLI prefix
data/lib/ukiryu/config.rb CHANGED
@@ -33,7 +33,9 @@ module Ukiryu
33
33
 
34
34
  class << self
35
35
  def instance
36
- @instance ||= new
36
+ mutex.synchronize do
37
+ @instance ||= new
38
+ end
37
39
  end
38
40
 
39
41
  # Configure Ukiryu with a block
@@ -46,7 +48,9 @@ module Ukiryu
46
48
 
47
49
  # Reset configuration to defaults
48
50
  def reset!
49
- @instance = new
51
+ mutex.synchronize do
52
+ @instance = new
53
+ end
50
54
  end
51
55
 
52
56
  # Delegate to instance
@@ -57,6 +61,13 @@ module Ukiryu
57
61
  def respond_to_missing?(method, include_private = false)
58
62
  instance.respond_to?(method) || super
59
63
  end
64
+
65
+ private
66
+
67
+ # Mutex for thread-safe singleton access
68
+ def mutex
69
+ @mutex ||= Mutex.new
70
+ end
60
71
  end
61
72
 
62
73
  # @!attribute [r] resolver
data/lib/ukiryu/debug.rb CHANGED
@@ -12,24 +12,35 @@ module Ukiryu
12
12
  #
13
13
  # @example Disable debug logging (default)
14
14
  # Ukiryu.debug_enabled? # => false
15
+ #
16
+ # @example Log with category
17
+ # Ukiryu::Debug.log("Found executable", category: :executable)
15
18
  module Debug
16
19
  class << self
17
- # Check if debug logging is enabled
18
- #
19
- # Debug is enabled ONLY when UKIRYU_DEBUG environment variable is set.
20
- # The ENV['CI'] check was removed to prevent debug output from polluting
21
- # JSON/YAML output in automated tests.
20
+ # Check if debug logging is enabled for a category
22
21
  #
22
+ # @param category [Symbol, nil] the category (:executable for UKIRYU_DEBUG_EXECUTABLE)
23
23
  # @return [Boolean] true if debug mode is enabled
24
- def enabled?
25
- ENV['UKIRYU_DEBUG'] || ENV['UKIRYU_DEBUG_EXECUTABLE']
24
+ def enabled?(category = nil)
25
+ case category
26
+ when :executable
27
+ ENV['UKIRYU_DEBUG_EXECUTABLE'] || (defined?(Platform) && Platform.windows? && ENV['CI'])
28
+ else
29
+ ENV['UKIRYU_DEBUG'] || ENV['UKIRYU_DEBUG_EXECUTABLE']
30
+ end
26
31
  end
27
32
 
28
33
  # Log a debug message to stderr
29
34
  #
30
35
  # @param message [String] the debug message
31
- def log(message)
32
- warn "[UKIRYU DEBUG] #{message}" if enabled?
36
+ # @param category [Symbol, nil] optional category (:executable for executable discovery)
37
+ # @param context [Hash] optional context data
38
+ def log(message, category: nil, context: {})
39
+ return unless enabled?(category)
40
+
41
+ prefix = "[UKIRYU DEBUG#{category ? " #{category.to_s.upcase}" : ''}]"
42
+ details = context.empty? ? '' : " (#{context.map { |k, v| "#{k}=#{v.inspect}" }.join(', ')})"
43
+ warn "#{prefix} #{message}#{details}"
33
44
  end
34
45
  end
35
46
  end
@@ -60,11 +60,11 @@ module Ukiryu
60
60
  load_from_source(source, options)
61
61
  end
62
62
 
63
- # Get the profile cache
63
+ # Get the profile cache (bounded LRU cache)
64
64
  #
65
- # @return [Hash] the profile cache
65
+ # @return [Cache] the profile cache
66
66
  def profile_cache
67
- @profile_cache ||= {}
67
+ @profile_cache ||= Cache.new(max_size: 100, ttl: 3600)
68
68
  end
69
69
 
70
70
  # Clear the profile cache