ukiryu 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/docs.yml +63 -0
  3. data/.github/workflows/links.yml +99 -0
  4. data/.github/workflows/rake.yml +19 -0
  5. data/.github/workflows/release.yml +27 -0
  6. data/.gitignore +18 -4
  7. data/.rubocop.yml +1 -0
  8. data/.rubocop_todo.yml +213 -0
  9. data/Gemfile +12 -8
  10. data/README.adoc +613 -0
  11. data/Rakefile +2 -2
  12. data/docs/assets/logo.svg +1 -0
  13. data/exe/ukiryu +11 -0
  14. data/lib/ukiryu/action/base.rb +77 -0
  15. data/lib/ukiryu/cache.rb +199 -0
  16. data/lib/ukiryu/cli.rb +133 -307
  17. data/lib/ukiryu/cli_commands/base_command.rb +155 -0
  18. data/lib/ukiryu/cli_commands/commands_command.rb +120 -0
  19. data/lib/ukiryu/cli_commands/commands_command.rb.fixed +40 -0
  20. data/lib/ukiryu/cli_commands/config_command.rb +249 -0
  21. data/lib/ukiryu/cli_commands/describe_command.rb +326 -0
  22. data/lib/ukiryu/cli_commands/describe_command.rb.fixed +254 -0
  23. data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +180 -0
  24. data/lib/ukiryu/cli_commands/extract_command.rb +84 -0
  25. data/lib/ukiryu/cli_commands/info_command.rb +156 -0
  26. data/lib/ukiryu/cli_commands/list_command.rb +70 -0
  27. data/lib/ukiryu/cli_commands/opts_command.rb +106 -0
  28. data/lib/ukiryu/cli_commands/opts_command.rb.fixed +105 -0
  29. data/lib/ukiryu/cli_commands/response_formatter.rb +240 -0
  30. data/lib/ukiryu/cli_commands/run_command.rb +375 -0
  31. data/lib/ukiryu/cli_commands/run_file_command.rb +215 -0
  32. data/lib/ukiryu/cli_commands/system_command.rb +90 -0
  33. data/lib/ukiryu/cli_commands/validate_command.rb +87 -0
  34. data/lib/ukiryu/cli_commands/version_command.rb +16 -0
  35. data/lib/ukiryu/cli_commands/which_command.rb +166 -0
  36. data/lib/ukiryu/command_builder.rb +205 -0
  37. data/lib/ukiryu/config/env_provider.rb +64 -0
  38. data/lib/ukiryu/config/env_schema.rb +63 -0
  39. data/lib/ukiryu/config/override_resolver.rb +68 -0
  40. data/lib/ukiryu/config/type_converter.rb +59 -0
  41. data/lib/ukiryu/config.rb +249 -0
  42. data/lib/ukiryu/errors.rb +3 -0
  43. data/lib/ukiryu/executable_locator.rb +114 -0
  44. data/lib/ukiryu/execution/command_info.rb +64 -0
  45. data/lib/ukiryu/execution/metadata.rb +97 -0
  46. data/lib/ukiryu/execution/output.rb +144 -0
  47. data/lib/ukiryu/execution/result.rb +194 -0
  48. data/lib/ukiryu/execution.rb +15 -0
  49. data/lib/ukiryu/execution_context.rb +251 -0
  50. data/lib/ukiryu/executor.rb +76 -493
  51. data/lib/ukiryu/extractors/base_extractor.rb +63 -0
  52. data/lib/ukiryu/extractors/extractor.rb +150 -0
  53. data/lib/ukiryu/extractors/help_parser.rb +188 -0
  54. data/lib/ukiryu/extractors/native_extractor.rb +47 -0
  55. data/lib/ukiryu/io.rb +196 -0
  56. data/lib/ukiryu/logger.rb +544 -0
  57. data/lib/ukiryu/models/argument.rb +28 -0
  58. data/lib/ukiryu/models/argument_definition.rb +119 -0
  59. data/lib/ukiryu/models/arguments.rb +113 -0
  60. data/lib/ukiryu/models/command_definition.rb +176 -0
  61. data/lib/ukiryu/models/command_info.rb +37 -0
  62. data/lib/ukiryu/models/components.rb +107 -0
  63. data/lib/ukiryu/models/env_var_definition.rb +30 -0
  64. data/lib/ukiryu/models/error_response.rb +41 -0
  65. data/lib/ukiryu/models/execution_metadata.rb +31 -0
  66. data/lib/ukiryu/models/execution_report.rb +236 -0
  67. data/lib/ukiryu/models/exit_codes.rb +74 -0
  68. data/lib/ukiryu/models/flag_definition.rb +67 -0
  69. data/lib/ukiryu/models/option_definition.rb +102 -0
  70. data/lib/ukiryu/models/output_info.rb +25 -0
  71. data/lib/ukiryu/models/platform_profile.rb +153 -0
  72. data/lib/ukiryu/models/routing.rb +211 -0
  73. data/lib/ukiryu/models/search_paths.rb +39 -0
  74. data/lib/ukiryu/models/success_response.rb +85 -0
  75. data/lib/ukiryu/models/tool_definition.rb +145 -0
  76. data/lib/ukiryu/models/tool_metadata.rb +82 -0
  77. data/lib/ukiryu/models/validation_result.rb +80 -0
  78. data/lib/ukiryu/models/version_compatibility.rb +152 -0
  79. data/lib/ukiryu/models/version_detection.rb +39 -0
  80. data/lib/ukiryu/models.rb +23 -0
  81. data/lib/ukiryu/options/base.rb +95 -0
  82. data/lib/ukiryu/options_builder/formatter.rb +87 -0
  83. data/lib/ukiryu/options_builder/validator.rb +43 -0
  84. data/lib/ukiryu/options_builder.rb +311 -0
  85. data/lib/ukiryu/platform.rb +6 -6
  86. data/lib/ukiryu/registry.rb +143 -183
  87. data/lib/ukiryu/response/base.rb +217 -0
  88. data/lib/ukiryu/runtime.rb +179 -0
  89. data/lib/ukiryu/schema_validator.rb +8 -10
  90. data/lib/ukiryu/shell/bash.rb +3 -3
  91. data/lib/ukiryu/shell/cmd.rb +4 -4
  92. data/lib/ukiryu/shell/fish.rb +1 -1
  93. data/lib/ukiryu/shell/powershell.rb +3 -3
  94. data/lib/ukiryu/shell/sh.rb +1 -1
  95. data/lib/ukiryu/shell/zsh.rb +1 -1
  96. data/lib/ukiryu/shell.rb +146 -39
  97. data/lib/ukiryu/thor_ext.rb +208 -0
  98. data/lib/ukiryu/tool.rb +649 -258
  99. data/lib/ukiryu/tool_index.rb +224 -0
  100. data/lib/ukiryu/tools/base.rb +381 -0
  101. data/lib/ukiryu/tools/class_generator.rb +132 -0
  102. data/lib/ukiryu/tools/executable_finder.rb +29 -0
  103. data/lib/ukiryu/tools/generator.rb +154 -0
  104. data/lib/ukiryu/tools.rb +109 -0
  105. data/lib/ukiryu/type.rb +28 -43
  106. data/lib/ukiryu/validation/constraints.rb +281 -0
  107. data/lib/ukiryu/validation/validator.rb +188 -0
  108. data/lib/ukiryu/validation.rb +21 -0
  109. data/lib/ukiryu/version.rb +1 -1
  110. data/lib/ukiryu/version_detector.rb +51 -0
  111. data/lib/ukiryu.rb +31 -15
  112. data/ukiryu-proposal.md +2952 -0
  113. data/ukiryu.gemspec +18 -14
  114. metadata +137 -5
  115. data/.github/workflows/test.yml +0 -143
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+ require_relative '../registry'
5
+ require_relative '../models/validation_result'
6
+
7
+ module Ukiryu
8
+ module CliCommands
9
+ # Schema validation command for tool profiles
10
+ class ValidateCommand < BaseCommand
11
+ # Execute the validate command
12
+ #
13
+ # @param tool_name [String, nil] the tool name (nil to validate all)
14
+ # @param options [Hash] command options
15
+ def run(tool_name = nil)
16
+ setup_registry
17
+
18
+ if tool_name
19
+ validate_single_tool(tool_name)
20
+ else
21
+ validate_all_tools
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ # Validate a single tool
28
+ #
29
+ # @param tool_name [String] the tool name
30
+ def validate_single_tool(tool_name)
31
+ result = Registry.validate_tool(tool_name, registry_path: config.registry)
32
+
33
+ say "Validating tool: #{tool_name}", :cyan
34
+ say '', :clear
35
+
36
+ if result.valid?
37
+ say result.status_message, :green
38
+ else
39
+ say result.status_message, :red
40
+ say '', :clear
41
+ result.errors.each do |error|
42
+ say " - #{error}", :white
43
+ end
44
+ end
45
+
46
+ # Exit with error code if validation failed
47
+ exit(result.invalid? ? 1 : 0)
48
+ end
49
+
50
+ # Validate all tools
51
+ def validate_all_tools
52
+ results = Registry.validate_all_tools(registry_path: config.registry)
53
+
54
+ say "Validating all tools in registry: #{config.registry}", :cyan
55
+ say '', :clear
56
+
57
+ valid_count = 0
58
+ invalid_count = 0
59
+ not_found_count = 0
60
+
61
+ results.each do |result|
62
+ say "#{result.tool_name.ljust(20)}: #{result.status_message}", result.valid? ? :green : :red
63
+
64
+ valid_count += 1 if result.valid?
65
+ invalid_count += 1 if result.invalid? && !result.not_found?
66
+ not_found_count += 1 if result.not_found?
67
+
68
+ # Show errors for invalid tools
69
+ if result.invalid? && !result.not_found?
70
+ result.errors.each do |error|
71
+ say " - #{error}", :dim
72
+ end
73
+ end
74
+ end
75
+
76
+ say '', :clear
77
+ say "Summary:", :cyan
78
+ say " Valid: #{valid_count}", :green
79
+ say " Invalid: #{invalid_count}", invalid_count > 0 ? :red : :white
80
+ say " Missing: #{not_found_count}", not_found_count > 0 ? :yellow : :white
81
+
82
+ # Exit with error code if any validations failed
83
+ exit(invalid_count > 0 ? 1 : 0)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+ require_relative '../version'
5
+
6
+ module Ukiryu
7
+ module CliCommands
8
+ # Show Ukiryu version
9
+ class VersionCommand < BaseCommand
10
+ # Execute the version command
11
+ def run
12
+ say "Ukiryu version #{Ukiryu::VERSION}", :cyan
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base_command'
4
+ require_relative '../tool'
5
+ require_relative '../platform'
6
+ require_relative '../runtime'
7
+
8
+ module Ukiryu
9
+ module CliCommands
10
+ # Show which tool implementation would be selected
11
+ class WhichCommand < BaseCommand
12
+ # Execute the which command
13
+ #
14
+ # @param identifier [String] the tool name, interface, or alias
15
+ def run(identifier)
16
+ setup_registry
17
+
18
+ runtime = Runtime.instance
19
+ platform = options[:platform] || runtime.platform
20
+ shell = options[:shell] || runtime.shell
21
+
22
+ say '', :clear
23
+ say "Resolving: #{identifier}", :cyan
24
+ say " Platform: #{platform}", :white
25
+ say " Shell: #{shell}", :white
26
+ say '', :clear
27
+
28
+ # First try exact name match
29
+ tool = try_exact_match(identifier, platform, shell)
30
+
31
+ # If not found, try interface/alias discovery
32
+ tool ||= try_interface_discovery(identifier, platform, shell)
33
+
34
+ if tool
35
+ show_selected_tool(tool, identifier, platform, shell)
36
+ else
37
+ error! "No tool found for: #{identifier}\nAvailable tools: #{Registry.tools.sort.join(', ')}"
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # Try exact name match
44
+ #
45
+ # @param identifier [String] the tool name
46
+ # @param platform [Symbol] the platform
47
+ # @param shell [Symbol] the shell
48
+ # @return [Tool, nil] the tool or nil if not found
49
+ def try_exact_match(identifier, platform, shell)
50
+ tool = Tool.get(identifier, platform: platform, shell: shell)
51
+ say 'Match type: Exact name match', :green
52
+ tool
53
+ rescue Ukiryu::ToolNotFoundError
54
+ nil
55
+ end
56
+
57
+ # Try interface/alias discovery
58
+ #
59
+ # @param identifier [String] the tool name
60
+ # @param platform [Symbol] the platform
61
+ # @param shell [Symbol] the shell
62
+ # @return [Tool, nil] the tool or nil if not found
63
+ def try_interface_discovery(identifier, platform, shell)
64
+ require_relative '../registry'
65
+
66
+ candidates = []
67
+
68
+ Registry.tools.each do |tool_name|
69
+ tool_metadata = Registry.load_tool_metadata(tool_name.to_sym)
70
+ next unless tool_metadata
71
+
72
+ # Check for interface match
73
+ interface_match = tool_metadata.implements == identifier.to_sym
74
+
75
+ # Check for alias match
76
+ alias_match = tool_metadata.aliases&.include?(identifier)
77
+
78
+ next unless interface_match || alias_match
79
+
80
+ # Check platform compatibility
81
+ profile = find_compatible_profile(tool_metadata, platform, shell)
82
+ next unless profile
83
+
84
+ match_type = interface_match ? 'interface' : 'alias'
85
+ candidates << {
86
+ name: tool_name,
87
+ metadata: tool_metadata,
88
+ profile: profile,
89
+ match_type: match_type
90
+ }
91
+ end
92
+
93
+ return nil if candidates.empty?
94
+
95
+ # Select best candidate (prefer available tools)
96
+ selected = candidates.find do |c|
97
+ Tool.get(c[:name], platform: platform, shell: shell).available?
98
+ end || candidates.first
99
+
100
+ say "Match type: #{selected[:match_type]} match", :green
101
+
102
+ Tool.get(selected[:name], platform: platform, shell: shell)
103
+ end
104
+
105
+ # Find compatible profile for platform/shell
106
+ #
107
+ # @param metadata [ToolMetadata] the tool metadata
108
+ # @param platform [Symbol] the platform
109
+ # @param shell [Symbol] the shell
110
+ # @return [Hash, nil] compatible profile or nil
111
+ def find_compatible_profile(metadata, platform, shell)
112
+ require_relative '../tools/generator'
113
+
114
+ tool_def = Tools::Generator.load_tool_definition(metadata.name)
115
+ return nil unless tool_def
116
+
117
+ tool_def.compatible_profile(platform: platform, shell: shell)
118
+ end
119
+
120
+ # Show selected tool information
121
+ #
122
+ # @param tool [Tool] the selected tool
123
+ # @param identifier [String] the original identifier
124
+ # @param platform [Symbol] the platform
125
+ # @param shell [Symbol] the shell
126
+ def show_selected_tool(tool, identifier, _platform, _shell)
127
+ say '', :clear
128
+ say 'Selected tool:', :yellow
129
+
130
+ if tool.name != identifier
131
+ say " Query: #{identifier}", :white
132
+ say " Resolved to: #{tool.name}", :white
133
+ else
134
+ say " Tool: #{tool.name}", :white
135
+ end
136
+
137
+ # Show implementation info
138
+ say " Implements: #{tool.profile.implements}", :white if tool.profile.implements
139
+
140
+ # Show profile used
141
+ profile = tool.instance_variable_get(:@command_profile)
142
+ if profile
143
+ say " Profile: #{profile.name || 'default'}", :white
144
+
145
+ # Show profile details
146
+ platforms = profile.platforms || ['all']
147
+ shells = profile.shells || ['all']
148
+ say " Platforms: #{Array(platforms).join(', ')}", :dim
149
+ say " Shells: #{Array(shells).join(', ')}", :dim
150
+ end
151
+
152
+ # Show availability
153
+ say '', :clear
154
+ if tool.available?
155
+ say 'Status: AVAILABLE', :green
156
+ say " Executable: #{tool.executable}", :white
157
+ detected_version = tool.version
158
+ say " Version: #{detected_version || 'unknown'}", :white if detected_version
159
+ else
160
+ say 'Status: NOT AVAILABLE', :red
161
+ say ' Tool is not installed or not in PATH', :dim
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'type'
4
+ require_relative 'shell'
5
+
6
+ module Ukiryu
7
+ # CommandBuilder module provides shared command building functionality.
8
+ #
9
+ # This module contains methods for building command-line arguments from
10
+ # command definitions and parameters. It is used by both Tool and
11
+ # Tools::Base to eliminate code duplication.
12
+ #
13
+ # @api private
14
+ module CommandBuilder
15
+ # Build command arguments from parameters
16
+ #
17
+ # @param command [Models::CommandDefinition] the command definition
18
+ # @param params [Hash] the parameters hash
19
+ # @return [Array<String>] the formatted command arguments
20
+ def build_args(command, params)
21
+ args = []
22
+
23
+ # Add subcommand prefix if present (e.g., for ImageMagick "magick convert")
24
+ args << command.subcommand if command.subcommand
25
+
26
+ # Add options first (before arguments)
27
+ command.options&.each do |opt_def|
28
+ param_key = opt_def.name_sym
29
+ next unless params.key?(param_key)
30
+ next if params[param_key].nil?
31
+
32
+ formatted_opt = format_option(opt_def, params[param_key])
33
+ Array(formatted_opt).each { |opt| args << opt unless opt.nil? || opt.empty? }
34
+ end
35
+
36
+ # Add flags
37
+ command.flags&.each do |flag_def|
38
+ param_key = flag_def.name_sym
39
+ value = params[param_key]
40
+ value = flag_def.default if value.nil?
41
+
42
+ formatted_flag = format_flag(flag_def, value)
43
+ Array(formatted_flag).each { |flag| args << flag unless flag.nil? || flag.empty? }
44
+ end
45
+
46
+ # Separate "last" positioned argument from other arguments
47
+ arguments = command.arguments || []
48
+ last_arg = arguments.find(&:last?)
49
+ regular_args = arguments.reject(&:last?)
50
+
51
+ # Add regular positional arguments (in order, excluding "last")
52
+ regular_args.sort_by(&:numeric_position).each do |arg_def|
53
+ param_key = arg_def.name_sym
54
+ next unless params.key?(param_key)
55
+
56
+ value = params[param_key]
57
+ next if value.nil?
58
+
59
+ if arg_def.variadic
60
+ # Variadic argument - expand array
61
+ array = Type.validate(value, :array, arg_def)
62
+ array.each { |v| args << format_arg(v, arg_def) }
63
+ else
64
+ args << format_arg(value, arg_def)
65
+ end
66
+ end
67
+
68
+ # Add post_options (options that come before the "last" argument)
69
+ command.post_options&.each do |opt_def|
70
+ param_key = opt_def.name_sym
71
+ next unless params.key?(param_key)
72
+ next if params[param_key].nil?
73
+
74
+ formatted_opt = format_option(opt_def, params[param_key])
75
+ Array(formatted_opt).each { |opt| args << opt unless opt.nil? || opt.empty? }
76
+ end
77
+
78
+ # Add the "last" positioned argument (typically output file)
79
+ if last_arg
80
+ param_key = last_arg.name_sym
81
+ if params.key?(param_key) && !params[param_key].nil?
82
+ if last_arg.variadic
83
+ array = Type.validate(params[param_key], :array, last_arg)
84
+ array.each { |v| args << format_arg(v, last_arg) }
85
+ else
86
+ args << format_arg(params[param_key], last_arg)
87
+ end
88
+ end
89
+ end
90
+
91
+ args
92
+ end
93
+
94
+ # Format a positional argument
95
+ #
96
+ # @param value [Object] the argument value
97
+ # @param arg_def [Models::ArgumentDefinition] the argument definition
98
+ # @return [String] the formatted argument
99
+ def format_arg(value, arg_def)
100
+ # Validate type
101
+ Type.validate(value, arg_def.type || :string, arg_def)
102
+
103
+ # Apply platform-specific path formatting
104
+ if arg_def.type == :file
105
+ shell_class = Shell.class_for(@shell)
106
+ shell_class.new.format_path(value.to_s)
107
+ else
108
+ value.to_s
109
+ end
110
+ end
111
+
112
+ # Format an option
113
+ #
114
+ # @param opt_def [Models::OptionDefinition] the option definition
115
+ # @param value [Object] the option value
116
+ # @return [String, Array<String>] the formatted option(s)
117
+ def format_option(opt_def, value)
118
+ # Validate type
119
+ Type.validate(value, opt_def.type || :string, opt_def)
120
+
121
+ # Handle boolean types - just return the CLI flag (no value)
122
+ type_val = opt_def.type
123
+ if [:boolean, TrueClass, 'boolean'].include?(type_val)
124
+ return nil if value.nil? || value == false
125
+
126
+ return opt_def.cli || ''
127
+ end
128
+
129
+ cli = opt_def.cli || ''
130
+ format_sym = opt_def.format_sym
131
+ separator = opt_def.separator || '='
132
+
133
+ # Convert value to string (handle symbols)
134
+ value_str = value.is_a?(Symbol) ? value.to_s : value.to_s
135
+
136
+ # Handle array values with separator
137
+ if value.is_a?(Array) && separator
138
+ joined = value.join(separator)
139
+ case format_sym
140
+ when :double_dash_equals
141
+ "#{cli}#{joined}"
142
+ when :double_dash_space, :single_dash_space
143
+ [cli, joined] # Return array for space-separated
144
+ when :single_dash_equals
145
+ "#{cli}#{joined}"
146
+ else
147
+ "#{cli}#{joined}"
148
+ end
149
+ else
150
+ case format_sym
151
+ when :double_dash_equals
152
+ "#{cli}#{separator}#{value_str}"
153
+ when :double_dash_space, :single_dash_space
154
+ [cli, value_str] # Return array for space-separated
155
+ when :single_dash_equals
156
+ "#{cli}#{separator}#{value_str}"
157
+ when :slash_colon
158
+ "#{cli}:#{value_str}"
159
+ when :slash_space
160
+ "#{cli} #{value_str}"
161
+ else
162
+ "#{cli}#{separator}#{value_str}"
163
+ end
164
+ end
165
+ end
166
+
167
+ # Format a flag
168
+ #
169
+ # @param flag_def [Models::FlagDefinition] the flag definition
170
+ # @param value [Object] the flag value
171
+ # @return [String, nil] the formatted flag
172
+ def format_flag(flag_def, value)
173
+ return nil if value.nil? || value == false
174
+
175
+ flag_def.cli || ''
176
+ end
177
+
178
+ # Build environment variables for command
179
+ #
180
+ # @param command [Models::CommandDefinition] the command definition
181
+ # @param params [Hash] the parameters hash
182
+ # @return [Hash] the environment variables hash
183
+ def build_env_vars(command, params)
184
+ env_vars = {}
185
+
186
+ command.env_vars&.each do |ev|
187
+ # Check platform restriction
188
+ platforms = ev.platforms || []
189
+ next if platforms.any? && !platforms.map(&:to_sym).include?(@platform)
190
+
191
+ # Get value - use ev.value if provided, or extract from params
192
+ value = if ev.value
193
+ ev.value
194
+ elsif ev.env_var
195
+ params[ev.env_var.to_sym]
196
+ end
197
+
198
+ # Set the environment variable if value is defined (including empty string)
199
+ env_vars[ev.name] = value.to_s unless value.nil?
200
+ end
201
+
202
+ env_vars
203
+ end
204
+ end
205
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'env_schema'
4
+ require_relative 'type_converter'
5
+
6
+ module Ukiryu
7
+ class Config
8
+ # Provides environment variable values for configuration
9
+ # Reads and parses UKIRYU_* environment variables and standard env vars like NO_COLOR
10
+ class EnvProvider
11
+ class << self
12
+ # Load all environment overrides
13
+ def load_all
14
+ result = {}
15
+
16
+ # Load UKIRYU_* environment variables
17
+ EnvSchema.all_attributes.each do |attr|
18
+ env_key = EnvSchema.env_key(attr)
19
+ value = ENV[env_key]
20
+
21
+ # Convert and store if value exists
22
+ result[attr] = TypeConverter.convert(attr, value) if value
23
+ end
24
+
25
+ # Handle NO_COLOR standard environment variable (https://no-color.org/)
26
+ # NO_COLOR takes precedence over UKIRYU_USE_COLOR
27
+ # When set (to any value), colors are disabled
28
+ result[:use_color] = false if ENV['NO_COLOR']
29
+
30
+ result
31
+ end
32
+
33
+ # Load execution-specific environment overrides
34
+ def load_execution
35
+ load_attributes(EnvSchema.all_execution_attributes)
36
+ end
37
+
38
+ # Load output-specific environment overrides
39
+ def load_output
40
+ load_attributes(EnvSchema.all_output_attributes)
41
+ end
42
+
43
+ # Load registry-specific environment overrides
44
+ def load_registry
45
+ load_attributes(EnvSchema.all_registry_attributes)
46
+ end
47
+
48
+ private
49
+
50
+ def load_attributes(attributes)
51
+ result = {}
52
+ attributes.each do |attr|
53
+ env_key = EnvSchema.env_key(attr)
54
+ value = ENV[env_key]
55
+
56
+ # Convert and store if value exists
57
+ result[attr] = TypeConverter.convert(attr, value) if value
58
+ end
59
+ result
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ukiryu
4
+ class Config
5
+ # Schema definition for configuration attributes
6
+ # Defines attribute types and ENV variable mappings
7
+ class EnvSchema
8
+ ATTRIBUTE_TYPES = {
9
+ # Execution options
10
+ timeout: :integer,
11
+ debug: :boolean,
12
+ dry_run: :boolean,
13
+ metrics: :boolean,
14
+ shell: :symbol,
15
+
16
+ # Output options
17
+ format: :symbol,
18
+ output: :string,
19
+
20
+ # Registry options
21
+ registry: :string,
22
+
23
+ # Tool discovery options
24
+ search_paths: :string, # Comma-separated paths
25
+
26
+ # Color options
27
+ use_color: :boolean
28
+ }.freeze
29
+
30
+ class << self
31
+ def type_for(attribute)
32
+ ATTRIBUTE_TYPES[attribute.to_sym]
33
+ end
34
+
35
+ # Generate ENV key for a config attribute
36
+ # e.g., env_key(:timeout) => "UKIRYU_TIMEOUT"
37
+ def env_key(attribute)
38
+ "UKIRYU_#{attribute.to_s.upcase}"
39
+ end
40
+
41
+ # All execution attributes
42
+ def all_execution_attributes
43
+ %i[timeout debug dry_run metrics shell]
44
+ end
45
+
46
+ # All output attributes
47
+ def all_output_attributes
48
+ %i[format output use_color]
49
+ end
50
+
51
+ # All registry attributes
52
+ def all_registry_attributes
53
+ %i[registry search_paths]
54
+ end
55
+
56
+ # All attributes
57
+ def all_attributes
58
+ %i[timeout debug dry_run metrics shell format output registry search_paths use_color]
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ukiryu
4
+ class Config
5
+ # Resolves configuration values using priority chain
6
+ # Priority: CLI options > ENV > programmatic > defaults
7
+ class OverrideResolver
8
+ attr_reader :defaults, :programmatic, :env, :cli
9
+
10
+ def initialize(defaults: {}, programmatic: {}, env: {}, cli: {})
11
+ @defaults = defaults
12
+ @programmatic = programmatic
13
+ @env = env
14
+ @cli = cli
15
+ end
16
+
17
+ # Resolve a single value using priority chain
18
+ # Uses .key? to properly handle false values
19
+ def resolve(key)
20
+ return @cli[key] if @cli.key?(key)
21
+ return @env[key] if @env.key?(key)
22
+ return @programmatic[key] if @programmatic.key?(key)
23
+
24
+ @defaults[key]
25
+ end
26
+
27
+ # Update programmatic value
28
+ def set_programmatic(key, value)
29
+ @programmatic[key] = value
30
+ end
31
+
32
+ # Update CLI option value
33
+ def set_cli(key, value)
34
+ @cli[key] = value
35
+ end
36
+
37
+ # Update ENV override
38
+ def set_env(key, value)
39
+ @env[key] = value
40
+ end
41
+
42
+ # Check if value is set by CLI
43
+ def cli_set?(key)
44
+ @cli.key?(key)
45
+ end
46
+
47
+ # Check if value is set by ENV
48
+ def env_set?(key)
49
+ @env.key?(key)
50
+ end
51
+
52
+ # Check if value is set programmatically
53
+ def programmatic_set?(key)
54
+ @programmatic.key?(key)
55
+ end
56
+
57
+ # Get the source of a value
58
+ def source_for(key)
59
+ return :cli if @cli.key?(key)
60
+ return :env if @env.key?(key)
61
+ return :programmatic if @programmatic.key?(key)
62
+ return :default if @defaults.key?(key)
63
+
64
+ nil
65
+ end
66
+ end
67
+ end
68
+ end