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,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+ require 'socket'
5
+ require 'etc'
6
+
7
+ module Ukiryu
8
+ module Models
9
+ # Metrics for a single stage of execution
10
+ class StageMetrics < Lutaml::Model::Serializable
11
+ attribute :name, :string, default: ''
12
+ attribute :duration, :float, default: 0.0
13
+ attribute :formatted_duration, :string, default: ''
14
+ attribute :memory_before, :integer, default: 0
15
+ attribute :memory_after, :integer, default: 0
16
+ attribute :memory_delta, :integer, default: 0
17
+ attribute :success, :boolean, default: true
18
+ attribute :error, :string, default: ''
19
+
20
+ yaml do
21
+ map_element 'name', to: :name
22
+ map_element 'duration', to: :duration
23
+ map_element 'formatted_duration', to: :formatted_duration
24
+ map_element 'memory_before', to: :memory_before
25
+ map_element 'memory_after', to: :memory_after
26
+ map_element 'memory_delta', to: :memory_delta
27
+ map_element 'success', to: :success
28
+ map_element 'error', to: :error
29
+ end
30
+
31
+ # Record the end of a stage
32
+ def finish!(success: true, error: nil)
33
+ @duration = Time.now - @start_time if @start_time
34
+ @formatted_duration = format_duration(@duration)
35
+ @success = success
36
+ @error = error if error
37
+
38
+ # Record memory after
39
+ @memory_after = get_memory_usage
40
+ @memory_delta = @memory_after - @memory_before
41
+ end
42
+
43
+ # Start recording this stage
44
+ def start!
45
+ @start_time = Time.now
46
+ @memory_before = get_memory_usage
47
+ self
48
+ end
49
+
50
+ private
51
+
52
+ def get_memory_usage
53
+ # Get RSS memory usage in KB
54
+ `ps -o rss= -p #{Process.pid}`.to_i
55
+ rescue StandardError
56
+ 0
57
+ end
58
+
59
+ def format_duration(seconds)
60
+ return '0ms' if seconds.nil? || seconds.zero?
61
+ return "#{(seconds * 1000).round(2)}ms" if seconds < 1
62
+
63
+ "#{seconds.round(2)}s"
64
+ end
65
+ end
66
+
67
+ # Run environment information
68
+ class RunEnvironment < Lutaml::Model::Serializable
69
+ attribute :hostname, :string, default: ''
70
+ attribute :platform, :string, default: ''
71
+ attribute :os_version, :string, default: ''
72
+ attribute :shell, :string, default: ''
73
+ attribute :shell_override, :boolean, default: false
74
+ attribute :shell_version, :string, default: ''
75
+ attribute :ruby_version, :string, default: ''
76
+ attribute :ukiryu_version, :string, default: ''
77
+ attribute :cpu_count, :integer, default: 0
78
+ attribute :total_memory, :integer, default: 0
79
+ attribute :working_directory, :string, default: ''
80
+
81
+ yaml do
82
+ map_element 'hostname', to: :hostname
83
+ map_element 'platform', to: :platform
84
+ map_element 'os_version', to: :os_version
85
+ map_element 'shell', to: :shell
86
+ map_element 'shell_override', to: :shell_override
87
+ map_element 'shell_version', to: :shell_version
88
+ map_element 'ruby_version', to: :ruby_version
89
+ map_element 'ukiryu_version', to: :ukiryu_version
90
+ map_element 'cpu_count', to: :cpu_count
91
+ map_element 'total_memory', to: :total_memory
92
+ map_element 'working_directory', to: :working_directory
93
+ end
94
+
95
+ # Collect all environment information
96
+ #
97
+ # @return [RunEnvironment] the environment info
98
+ def self.collect
99
+ require_relative '../runtime'
100
+ require_relative '../config'
101
+ require_relative '../shell'
102
+
103
+ runtime = Runtime.instance
104
+ begin
105
+ Shell.detect
106
+ rescue Ukiryu::UnknownShellError
107
+ 'unknown'
108
+ end
109
+
110
+ # Determine if shell was overridden
111
+ shell_override = !Config.shell.nil?
112
+ actual_shell = runtime.shell.to_s
113
+
114
+ new(
115
+ hostname: Socket.gethostname,
116
+ platform: RUBY_PLATFORM,
117
+ os_version: os_version_string,
118
+ shell: actual_shell,
119
+ shell_override: shell_override,
120
+ shell_version: detect_shell_version_for(actual_shell),
121
+ ruby_version: RUBY_VERSION,
122
+ ukiryu_version: Ukiryu::VERSION,
123
+ cpu_count: Etc.nprocessors,
124
+ total_memory: detect_total_memory,
125
+ working_directory: Dir.pwd
126
+ )
127
+ end
128
+
129
+ # Detect shell version for a specific shell
130
+ #
131
+ # @param shell_name [String] the shell name
132
+ # @return [String] the shell version string
133
+ def self.detect_shell_version_for(shell_name)
134
+ return '' if shell_name == 'unknown' || shell_name.empty?
135
+
136
+ shell_path = ENV['SHELL']
137
+ return '' unless shell_path
138
+
139
+ `#{shell_path} --version 2>&1`.strip
140
+ rescue StandardError
141
+ ''
142
+ end
143
+
144
+ # Get OS version string
145
+ #
146
+ # @return [String] the OS version
147
+ def self.os_version_string
148
+ # Try to get OS version from RbConfig
149
+ RbConfig::CONFIG['host_os'] || RbConfig::CONFIG['target_os'] || RUBY_PLATFORM
150
+ end
151
+
152
+ # Detect total system memory
153
+ #
154
+ # @return [Integer] total memory in GB
155
+ def self.detect_total_memory
156
+ # Get total system memory in GB
157
+ if RUBY_PLATFORM =~ /darwin/i
158
+ # macOS
159
+ `sysctl hw.memsize`.to_i / (1024**3)
160
+ elsif RUBY_PLATFORM =~ /linux/i
161
+ # Linux
162
+ `grep MemTotal /proc/meminfo`.split[1].to_i / 1024
163
+ else
164
+ 0
165
+ end
166
+ rescue StandardError
167
+ 0
168
+ end
169
+
170
+ private_class_method :detect_shell_version_for, :detect_total_memory, :os_version_string
171
+ end
172
+
173
+ # Execution report containing metrics and timing information
174
+ #
175
+ # Provides detailed metrics about the execution process including:
176
+ # - Stage timings (tool resolution, command building, execution)
177
+ # - Memory usage
178
+ # - Run environment information
179
+ class ExecutionReport < Lutaml::Model::Serializable
180
+ attribute :tool_resolution, StageMetrics
181
+ attribute :command_building, StageMetrics
182
+ attribute :execution, StageMetrics
183
+ attribute :response_building, StageMetrics
184
+ attribute :total_duration, :float, default: 0.0
185
+ attribute :formatted_total_duration, :string, default: ''
186
+ attribute :run_environment, RunEnvironment
187
+ attribute :timestamp, :string, default: ''
188
+
189
+ yaml do
190
+ map_element 'tool_resolution', to: :tool_resolution
191
+ map_element 'command_building', to: :command_building
192
+ map_element 'execution', to: :execution
193
+ map_element 'response_building', to: :response_building
194
+ map_element 'total_duration', to: :total_duration
195
+ map_element 'formatted_total_duration', to: :formatted_total_duration
196
+ map_element 'run_environment', to: :run_environment
197
+ map_element 'timestamp', to: :timestamp
198
+ end
199
+
200
+ json do
201
+ map 'tool_resolution', to: :tool_resolution
202
+ map 'command_building', to: :command_building
203
+ map 'execution', to: :execution
204
+ map 'response_building', to: :response_building
205
+ map 'total_duration', to: :total_duration
206
+ map 'formatted_total_duration', to: :formatted_total_duration
207
+ map 'run_environment', to: :run_environment
208
+ map 'timestamp', to: :timestamp
209
+ end
210
+
211
+ # Calculate total duration from all stages
212
+ def calculate_total
213
+ stages = [tool_resolution, command_building, execution, response_building]
214
+ total = stages.compact.map(&:duration).sum
215
+ @total_duration = total
216
+ @formatted_total_duration = format_duration(total)
217
+ end
218
+
219
+ # Get all stages in order
220
+ #
221
+ # @return [Array<StageMetrics>] all stages
222
+ def all_stages
223
+ [tool_resolution, command_building, execution, response_building].compact
224
+ end
225
+
226
+ private
227
+
228
+ def format_duration(seconds)
229
+ return '0ms' if seconds.zero?
230
+ return "#{(seconds * 1000).round(2)}ms" if seconds < 1
231
+
232
+ "#{seconds.round(2)}s"
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+
5
+ module Ukiryu
6
+ module Models
7
+ # Exit code definitions for tool commands
8
+ #
9
+ # Provides machine-readable error semantics for exit codes.
10
+ #
11
+ # @example
12
+ # exit_codes = ExitCodes.new(
13
+ # standard: { '0' => 'success', '1' => 'general_error' },
14
+ # custom: { '3' => 'merge_conflict', '4' => 'permission_denied' }
15
+ # )
16
+ class ExitCodes < Lutaml::Model::Serializable
17
+ attribute :standard, :hash, default: {}
18
+ attribute :custom, :hash, default: {}
19
+
20
+ yaml do
21
+ map_element 'standard', to: :standard
22
+ map_element 'custom', to: :custom
23
+ end
24
+
25
+ # Get the meaning of an exit code
26
+ #
27
+ # @param code [Integer] the exit code
28
+ # @return [String, nil] the meaning or nil if not defined
29
+ def meaning(code)
30
+ code_str = code.to_s
31
+
32
+ # Check custom codes first (more specific)
33
+ @custom&.dig(code_str) || @standard&.dig(code_str)
34
+ end
35
+
36
+ # Check if an exit code is defined
37
+ #
38
+ # @param code [Integer] the exit code
39
+ # @return [Boolean] true if defined
40
+ def defined?(code)
41
+ !meaning(code).nil?
42
+ end
43
+
44
+ # Check if an exit code indicates success
45
+ #
46
+ # @param code [Integer] the exit code
47
+ # @return [Boolean] true if success (0 or defined as success)
48
+ def success?(code)
49
+ code.zero? || meaning(code) == 'success'
50
+ end
51
+
52
+ # Get all defined exit codes
53
+ #
54
+ # @return [Hash] all codes merged (standard + custom)
55
+ def all_codes
56
+ @standard.to_h.merge(@custom.to_h)
57
+ end
58
+
59
+ # Get standard exit codes
60
+ #
61
+ # @return [Hash] standard codes
62
+ def standard_codes
63
+ @standard.to_h
64
+ end
65
+
66
+ # Get custom exit codes
67
+ #
68
+ # @return [Hash] custom codes
69
+ def custom_codes
70
+ @custom.to_h
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+
5
+ module Ukiryu
6
+ module Models
7
+ # Flag definition for a command
8
+ #
9
+ # Represents a boolean flag (present or absent)
10
+ #
11
+ # @example
12
+ # flag = FlagDefinition.new(
13
+ # name: 'verbose',
14
+ # cli: '-v',
15
+ # default: false,
16
+ # description: 'Enable verbose output'
17
+ # )
18
+ class FlagDefinition < Lutaml::Model::Serializable
19
+ attribute :name, :string
20
+ attribute :cli, :string
21
+ attribute :default, :boolean, default: false
22
+ attribute :description, :string
23
+ attribute :platforms, :string, collection: true, default: []
24
+
25
+ yaml do
26
+ map_element 'name', to: :name
27
+ map_element 'cli', to: :cli
28
+ map_element 'default', to: :default
29
+ map_element 'description', to: :description
30
+ map_element 'platforms', to: :platforms
31
+ end
32
+
33
+ # Get the effective default value
34
+ #
35
+ # @return [Boolean] the default value
36
+ def default_value
37
+ default || false
38
+ end
39
+
40
+ # Check if flag applies to a platform
41
+ #
42
+ # @param platform [Symbol] the platform
43
+ # @return [Boolean] true if applies
44
+ def applies_to?(platform)
45
+ return true if platforms.nil? || platforms.empty?
46
+
47
+ cached_platforms_sym.include?(platform.to_sym)
48
+ end
49
+
50
+ # Get name as symbol (cached for performance)
51
+ #
52
+ # @return [Symbol] the name as symbol
53
+ def name_sym
54
+ @name_sym ||= name.to_sym
55
+ end
56
+
57
+ private
58
+
59
+ # Get platforms as cached symbol array
60
+ #
61
+ # @api private
62
+ def cached_platforms_sym
63
+ @cached_platforms_sym ||= platforms&.map(&:to_sym) || []
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+
5
+ module Ukiryu
6
+ module Models
7
+ # Option definition for a command
8
+ #
9
+ # Represents a named option (flag with value)
10
+ #
11
+ # @example
12
+ # opt = OptionDefinition.new(
13
+ # name: 'quality',
14
+ # cli: '-q',
15
+ # type: 'integer',
16
+ # format: 'single_dash_space',
17
+ # description: 'JPEG quality'
18
+ # )
19
+ class OptionDefinition < Lutaml::Model::Serializable
20
+ attribute :name, :string
21
+ attribute :cli, :string
22
+ attribute :type, :string, default: 'string'
23
+ attribute :format, :string, default: 'single_dash_space'
24
+ attribute :separator, :string
25
+ attribute :default, :string
26
+ # Array for numeric range [min, max]
27
+ attribute :range, :integer, collection: true
28
+ # Valid values for symbols
29
+ attribute :values, :string, collection: true
30
+ # Type of array elements
31
+ attribute :of, :string
32
+ attribute :description, :string
33
+ attribute :platforms, :string, collection: true, default: []
34
+
35
+ yaml do
36
+ map_element 'name', to: :name
37
+ map_element 'cli', to: :cli
38
+ map_element 'type', to: :type
39
+ map_element 'format', to: :format
40
+ map_element 'separator', to: :separator
41
+ map_element 'default', to: :default
42
+ map_element 'range', to: :range
43
+ map_element 'values', to: :values
44
+ map_element 'of', to: :of
45
+ map_element 'description', to: :description
46
+ map_element 'platforms', to: :platforms
47
+ end
48
+
49
+ # Check if option applies to a platform
50
+ #
51
+ # @param platform [Symbol] the platform
52
+ # @return [Boolean] true if applies
53
+ def applies_to?(platform)
54
+ return true if platforms.nil? || platforms.empty?
55
+
56
+ cached_platforms_sym.include?(platform.to_sym)
57
+ end
58
+
59
+ # Check if type is boolean
60
+ #
61
+ # @return [Boolean] true if boolean type
62
+ def boolean?
63
+ type == 'boolean'
64
+ end
65
+
66
+ # Get format as symbol (cached for performance)
67
+ #
68
+ # @return [Symbol] the format
69
+ def format_sym
70
+ @format_sym ||= format&.to_sym || :single_dash_space
71
+ end
72
+
73
+ # Hash-like access for Type validation compatibility
74
+ #
75
+ # @param key [Symbol, String] the attribute key
76
+ # @return [Object] the attribute value
77
+ def [](key)
78
+ key_sym = key.to_sym
79
+ # Return nil for unknown keys (like Type validation options)
80
+ return nil unless respond_to?(key_sym, true)
81
+
82
+ send(key_sym)
83
+ end
84
+
85
+ # Get name as symbol (cached for performance)
86
+ #
87
+ # @return [Symbol] the name as symbol
88
+ def name_sym
89
+ @name_sym ||= name.to_sym
90
+ end
91
+
92
+ private
93
+
94
+ # Get platforms as cached symbol array
95
+ #
96
+ # @api private
97
+ def cached_platforms_sym
98
+ @cached_platforms_sym ||= platforms&.map(&:to_sym) || []
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+
5
+ module Ukiryu
6
+ module Models
7
+ # Command output information
8
+ #
9
+ # Contains the standard output and error output from command execution.
10
+ class OutputInfo < Lutaml::Model::Serializable
11
+ attribute :stdout, :string, default: ''
12
+ attribute :stderr, :string, default: ''
13
+
14
+ yaml do
15
+ map_element 'stdout', to: :stdout
16
+ map_element 'stderr', to: :stderr
17
+ end
18
+
19
+ json do
20
+ map 'stdout', to: :stdout
21
+ map 'stderr', to: :stderr
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+ require_relative 'command_definition'
5
+ require_relative 'routing'
6
+ require_relative 'exit_codes'
7
+
8
+ module Ukiryu
9
+ module Models
10
+ # Platform-specific profile for a tool
11
+ #
12
+ # @example
13
+ # profile = PlatformProfile.new(
14
+ # name: 'default',
15
+ # platforms: [:macos, :linux],
16
+ # commands: [CommandDefinition.new(...)]
17
+ # )
18
+ class PlatformProfile < Lutaml::Model::Serializable
19
+ attribute :name, :string
20
+ attribute :display_name, :string
21
+ attribute :platforms, :string, collection: true, default: []
22
+ attribute :shells, :string, collection: true, default: []
23
+ attribute :option_style, :string, default: 'single_dash_space'
24
+ attribute :commands, CommandDefinition, collection: true, initialize_empty: true
25
+ attribute :inherits, :string
26
+ attribute :routing_data, :hash, default: {} # Raw routing from YAML
27
+ attribute :version_requirement, :string # Semantic version requirement (e.g., ">= 2.30")
28
+ attribute :exit_codes, ExitCodes # Exit code definitions for this profile
29
+
30
+ yaml do
31
+ map_element 'name', to: :name
32
+ map_element 'display_name', to: :display_name
33
+ map_element 'platforms', to: :platforms
34
+ map_element 'shells', to: :shells
35
+ map_element 'option_style', to: :option_style
36
+ map_element 'commands', to: :commands
37
+ map_element 'inherits', to: :inherits
38
+ map_element 'routing', to: :routing_data
39
+ map_element 'version_requirement', to: :version_requirement
40
+ map_element 'exit_codes', to: :exit_codes
41
+ end
42
+
43
+ # Get the routing table as a Routing model
44
+ #
45
+ # @return [Routing] the routing table
46
+ def routing
47
+ @routing ||= Routing.new(@routing_data || {})
48
+ end
49
+
50
+ # Check if this profile has routing defined
51
+ #
52
+ # @return [Boolean] true if routing table is non-empty
53
+ def routing?
54
+ !@routing_data.nil? && !@routing_data.empty?
55
+ end
56
+
57
+ # Check if profile supports a platform
58
+ #
59
+ # @param platform [Symbol] the platform
60
+ # @return [Boolean] true if supported
61
+ def supports_platform?(platform)
62
+ platform_list = cached_platforms_sym
63
+ platform_list.nil? || platform_list.empty? ||
64
+ platform_list.include?(platform.to_sym)
65
+ end
66
+
67
+ # Check if profile supports a shell
68
+ #
69
+ # @param shell [Symbol] the shell
70
+ # @return [Boolean] true if supported
71
+ def supports_shell?(shell)
72
+ shell_list = cached_shells_sym
73
+ shell_list.nil? || shell_list.empty? ||
74
+ shell_list.include?(shell.to_sym)
75
+ end
76
+
77
+ # Check if profile is compatible with platform and shell
78
+ #
79
+ # @param platform [Symbol] the platform
80
+ # @param shell [Symbol] the shell
81
+ # @return [Boolean] true if compatible
82
+ def compatible?(platform, shell)
83
+ supports_platform?(platform) && supports_shell?(shell)
84
+ end
85
+
86
+ # Get a command by name using indexed O(1) lookup
87
+ #
88
+ # @param name [String, Symbol] the command name
89
+ # @return [CommandDefinition, nil] the command
90
+ def command(name)
91
+ return nil unless commands
92
+
93
+ build_commands_index unless @commands_index_built
94
+ @commands_index[name.to_s]
95
+ end
96
+
97
+ # Get all command names
98
+ #
99
+ # @return [Array<String>] command names
100
+ def command_names
101
+ return [] unless commands
102
+
103
+ build_commands_index unless @commands_index_built
104
+ @commands_index.keys
105
+ end
106
+
107
+ # Check if universal (supports all)
108
+ #
109
+ # @return [Boolean] true if universal
110
+ def universal?
111
+ (platforms.nil? || platforms.empty?) &&
112
+ (shells.nil? || shells.empty?)
113
+ end
114
+
115
+ # Clear the commands index
116
+ #
117
+ # Call this if commands are modified after initial loading
118
+ # (e.g., during inheritance resolution)
119
+ #
120
+ # @api private
121
+ def clear_commands_index!
122
+ @commands_index = nil
123
+ @commands_index_built = false
124
+ end
125
+
126
+ private
127
+
128
+ # Get platforms as cached symbol array
129
+ #
130
+ # @api private
131
+ def cached_platforms_sym
132
+ @cached_platforms_sym ||= platforms&.map(&:to_sym)
133
+ end
134
+
135
+ # Get shells as cached symbol array
136
+ #
137
+ # @api private
138
+ def cached_shells_sym
139
+ @cached_shells_sym ||= shells&.map(&:to_sym)
140
+ end
141
+
142
+ # Build the commands index hash for O(1) lookup
143
+ #
144
+ # @api private
145
+ def build_commands_index
146
+ return unless commands
147
+
148
+ @commands_index = commands.to_h { |c| [c.name, c] }
149
+ @commands_index_built = true
150
+ end
151
+ end
152
+ end
153
+ end