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,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ukiryu
4
+ module Models
5
+ # Represents a command routing table for hierarchical tools.
6
+ #
7
+ # Routing maps command names to their executable targets, enabling
8
+ # tools like git where `git remote` routes to `git-remote` executable.
9
+ #
10
+ # @example Basic routing
11
+ # routing = Routing.new({ 'remote' => 'git-remote', 'branch' => 'git-branch' })
12
+ # routing.resolve('remote') # => 'git-remote'
13
+ #
14
+ # @example Multi-level routing
15
+ # routing = Routing.new({ 'remote' => 'git-remote' })
16
+ # routing.child('remote').merge!({ 'add' => 'action' })
17
+ # routing.resolve_path(['remote', 'add']) # => ['git-remote', 'action']
18
+ #
19
+ class Routing
20
+ # The routing table mapping command names to executables
21
+ #
22
+ # @return [Hash<String, String>]
23
+ attr_reader :table
24
+
25
+ # Parent routing table for multi-level hierarchies
26
+ #
27
+ # @return [Routing, nil]
28
+ attr_reader :parent
29
+
30
+ # Create a new Routing table
31
+ #
32
+ # @param table [Hash] routing table mapping command names to executables
33
+ # @param parent [Routing, nil] parent routing for multi-level hierarchies
34
+ #
35
+ # @example
36
+ # Routing.new({ 'remote' => 'git-remote' })
37
+ # Routing.new({ 'add' => 'action' }, parent_routing)
38
+ #
39
+ def initialize(table = {}, parent: nil)
40
+ @table = symbolize_keys(table)
41
+ @parent = parent
42
+ @children = {}
43
+ end
44
+
45
+ # Resolve a command name to its executable target
46
+ #
47
+ # @param command_name [String, Symbol] the command name to resolve
48
+ # @return [String, nil] the executable target or nil if not found
49
+ #
50
+ # @example
51
+ # routing.resolve('remote') # => 'git-remote'
52
+ # routing.resolve('unknown') # => nil
53
+ #
54
+ def resolve(command_name)
55
+ key = command_name.to_sym
56
+ @table[key] || @parent&.resolve(key)
57
+ end
58
+
59
+ # Resolve a path of command names to their executable targets
60
+ #
61
+ # @param path [Array<String, Symbol>] the command path to resolve
62
+ # @return [Array<String>] array of executable targets
63
+ #
64
+ # @example
65
+ # routing.resolve_path(['remote', 'add']) # => ['git-remote', 'action']
66
+ #
67
+ def resolve_path(path)
68
+ return [] if path.empty?
69
+
70
+ # Resolve first level in this routing table
71
+ first_target = resolve(path.first)
72
+ return [] unless first_target
73
+
74
+ # If there are more levels, resolve them in child routing
75
+ if path.size > 1
76
+ child = child(path.first)
77
+ [first_target, *child.resolve_path(path[1..])]
78
+ else
79
+ [first_target]
80
+ end
81
+ end
82
+
83
+ # Check if a command exists in the routing table
84
+ #
85
+ # @param command_name [String, Symbol] the command name to check
86
+ # @return [Boolean] true if the command exists
87
+ #
88
+ # @example
89
+ # routing.include?('remote') # => true
90
+ # routing.include?('unknown') # => false
91
+ #
92
+ def include?(command_name)
93
+ key = command_name.to_sym
94
+ @table.key?(key) || @parent&.include?(key) || false
95
+ end
96
+
97
+ # Get a child routing table for a command
98
+ #
99
+ # Creates or returns the child routing for multi-level hierarchies.
100
+ #
101
+ # @param command_name [String, Symbol] the parent command name
102
+ # @return [Routing] the child routing table
103
+ #
104
+ # @example
105
+ # routing.child('remote').merge!({ 'add' => 'action' })
106
+ #
107
+ def child(command_name)
108
+ key = command_name.to_sym
109
+ @children[key] ||= Routing.new(parent: self)
110
+ end
111
+
112
+ # Merge a hash into this routing table
113
+ #
114
+ # @param other [Hash] the routing entries to merge
115
+ # @return [self] returns self for chaining
116
+ #
117
+ # @example
118
+ # routing.merge!({ 'branch' => 'git-branch' })
119
+ #
120
+ def merge!(other)
121
+ @table.merge!(symbolize_keys(other))
122
+ self
123
+ end
124
+
125
+ # Get all command names in this routing table
126
+ #
127
+ # @return [Array<String>] array of command names
128
+ #
129
+ # @example
130
+ # routing.keys # => ['remote', 'branch', 'stash']
131
+ #
132
+ def keys
133
+ @table.keys.map(&:to_s).sort
134
+ end
135
+
136
+ # Check if the routing table is empty
137
+ #
138
+ # @return [Boolean] true if no routing entries
139
+ #
140
+ def empty?
141
+ @table.empty?
142
+ end
143
+
144
+ # Get the number of routing entries
145
+ #
146
+ # @return [Integer] number of entries
147
+ #
148
+ def size
149
+ @table.size
150
+ end
151
+
152
+ # Convert routing table to hash
153
+ #
154
+ # @return [Hash] the routing table as a hash
155
+ #
156
+ def to_h
157
+ @table.transform_keys(&:to_s)
158
+ end
159
+
160
+ # Check for circular references in routing hierarchy
161
+ #
162
+ # @return [Boolean] true if circular reference detected
163
+ #
164
+ def circular?
165
+ return false unless @parent
166
+
167
+ current = @parent
168
+ seen = { self => true }
169
+
170
+ while current
171
+ return true if seen.key?(current)
172
+
173
+ seen[current] = true
174
+ current = current.parent
175
+ end
176
+
177
+ false
178
+ end
179
+
180
+ # String representation
181
+ #
182
+ # @return [String] debug-friendly string representation
183
+ #
184
+ def inspect
185
+ parent_info = @parent ? "(parent: #{parent.object_id})" : ''
186
+ "#<#{self.class.name}:#{object_id}#{parent_info} #{@table.keys.inspect}>"
187
+ end
188
+
189
+ # String representation for debugging
190
+ #
191
+ # @return [String] routing table as formatted string
192
+ #
193
+ def to_s
194
+ return "(empty)" if @table.empty?
195
+
196
+ @table.map { |k, v| "#{k} => #{v}" }.join(', ')
197
+ end
198
+
199
+ private
200
+
201
+ # Symbolize hash keys
202
+ #
203
+ # @param hash [Hash] the hash to symbolize
204
+ # @return [Hash] hash with symbolized keys
205
+ #
206
+ def symbolize_keys(hash)
207
+ hash.transform_keys(&:to_sym)
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+
5
+ module Ukiryu
6
+ module Models
7
+ # Search paths configuration for finding tool executables
8
+ #
9
+ # @example
10
+ # sp = SearchPaths.new
11
+ # sp.macos = ['/opt/homebrew/bin/tool']
12
+ # sp.linux = ['/usr/bin/tool']
13
+ class SearchPaths < Lutaml::Model::Serializable
14
+ attribute :macos, :string, collection: true, default: []
15
+ attribute :linux, :string, collection: true, default: []
16
+ attribute :windows, :string, collection: true, default: []
17
+ attribute :freebsd, :string, collection: true, default: []
18
+ attribute :openbsd, :string, collection: true, default: []
19
+ attribute :netbsd, :string, collection: true, default: []
20
+
21
+ yaml do
22
+ map_element 'macos', to: :macos
23
+ map_element 'linux', to: :linux
24
+ map_element 'windows', to: :windows
25
+ map_element 'freebsd', to: :freebsd
26
+ map_element 'openbsd', to: :openbsd
27
+ map_element 'netbsd', to: :netbsd
28
+ end
29
+
30
+ # Get search paths for a specific platform
31
+ #
32
+ # @param platform [Symbol] the platform
33
+ # @return [Array<String>] the search paths
34
+ def for_platform(platform)
35
+ send(platform) || []
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+ require_relative 'arguments'
5
+ require_relative 'command_info'
6
+ require_relative 'output_info'
7
+ require_relative 'execution_metadata'
8
+ require_relative 'execution_report'
9
+
10
+ module Ukiryu
11
+ module Models
12
+ # Successful CLI execution response
13
+ #
14
+ # Contains the complete result of a successful command execution
15
+ # structured into three phases: Request, Command, and Output.
16
+ # Optionally includes ExecutionReport with metrics when enabled.
17
+ class SuccessResponse < Lutaml::Model::Serializable
18
+ attribute :status, :string, default: 'success'
19
+ attribute :exit_code, :integer
20
+ attribute :request, Arguments
21
+ attribute :command, CommandInfo
22
+ attribute :output, OutputInfo
23
+ attribute :metadata, ExecutionMetadata
24
+ attribute :execution_report, ExecutionReport
25
+
26
+ yaml do
27
+ map_element 'status', to: :status
28
+ map_element 'exit_code', to: :exit_code
29
+ map_element 'request', to: :request
30
+ map_element 'command', to: :command
31
+ map_element 'output', to: :output
32
+ map_element 'metadata', to: :metadata
33
+ map_element 'execution_report', to: :execution_report
34
+ end
35
+
36
+ json do
37
+ map 'status', to: :status
38
+ map 'exit_code', to: :exit_code
39
+ map 'request', to: :request
40
+ map 'command', to: :command
41
+ map 'output', to: :output
42
+ map 'metadata', to: :metadata
43
+ map 'execution_report', to: :execution_report
44
+ end
45
+
46
+ # Create a SuccessResponse from an Executor::Result
47
+ #
48
+ # @param result [Executor::Result] the execution result
49
+ # @param params [Hash] the original parameters passed to the command
50
+ # @param command_definition [CommandDefinition] the command definition for context
51
+ # @param execution_report [ExecutionReport, nil] optional execution report with metrics
52
+ # @return [SuccessResponse] the response model
53
+ def self.from_result(result, params = {}, command_definition = nil, execution_report: nil)
54
+ request = Arguments.from_params(params, command_definition)
55
+
56
+ response = new(
57
+ status: 'success',
58
+ exit_code: result.status,
59
+ request: request,
60
+ command: CommandInfo.new(
61
+ executable: result.executable,
62
+ arguments: request, # Use the same structured arguments for the command
63
+ full_command: result.command_info.full_command,
64
+ shell: result.command_info.shell.to_s
65
+ ),
66
+ output: OutputInfo.new(
67
+ stdout: result.stdout,
68
+ stderr: result.error_output
69
+ ),
70
+ metadata: ExecutionMetadata.new(
71
+ started_at: result.started_at.iso8601,
72
+ finished_at: result.finished_at.iso8601,
73
+ duration_seconds: result.duration,
74
+ formatted_duration: result.metadata.formatted_duration
75
+ )
76
+ )
77
+
78
+ # Add execution report if provided
79
+ response.execution_report = execution_report if execution_report
80
+
81
+ response
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+ require_relative 'platform_profile'
5
+ require_relative 'version_detection'
6
+ require_relative 'search_paths'
7
+ require_relative 'components'
8
+
9
+ module Ukiryu
10
+ module Models
11
+ # Tool definition loaded from YAML profile
12
+ #
13
+ # @example
14
+ # tool = ToolDefinition.from_yaml(yaml_string)
15
+ # profile = tool.compatible_profile
16
+ class ToolDefinition < Lutaml::Model::Serializable
17
+ attribute :ukiryu_schema, :string
18
+ attribute :self_uri, :string
19
+ attribute :name, :string
20
+ attribute :display_name, :string
21
+ attribute :homepage, :string
22
+ attribute :version, :string
23
+ attribute :implements, :string
24
+ attribute :aliases, :string, collection: true, default: []
25
+ attribute :timeout, :integer, default: 90
26
+ attribute :profiles, PlatformProfile, collection: true
27
+ attribute :version_detection, VersionDetection
28
+ attribute :search_paths, SearchPaths
29
+ attribute :components, Components # Registry of reusable definitions
30
+
31
+ yaml do
32
+ map_element 'ukiryu_schema', to: :ukiryu_schema
33
+ map_element '$self', to: :self_uri
34
+ map_element 'name', to: :name
35
+ map_element 'display_name', to: :display_name
36
+ map_element 'homepage', to: :homepage
37
+ map_element 'version', to: :version
38
+ map_element 'implements', to: :implements
39
+ map_element 'aliases', to: :aliases
40
+ map_element 'timeout', to: :timeout
41
+ map_element 'profiles', to: :profiles
42
+ map_element 'version_detection', to: :version_detection
43
+ map_element 'search_paths', to: :search_paths
44
+ map_element 'components', to: :components
45
+ end
46
+
47
+ # Get compatible profile for current platform/shell
48
+ #
49
+ # @param platform [Symbol] the platform
50
+ # @param shell [Symbol] the shell
51
+ # @return [PlatformProfile, nil] the compatible profile
52
+ def compatible_profile(platform: nil, shell: nil)
53
+ require_relative '../platform'
54
+ require_relative '../shell'
55
+
56
+ platform ||= Platform.detect
57
+ shell ||= Shell.detect
58
+ return nil unless platform && shell
59
+
60
+ return nil if profiles.empty?
61
+
62
+ profiles.find do |p|
63
+ p.is_a?(PlatformProfile) && p.compatible?(platform.to_sym, shell.to_sym)
64
+ end
65
+ end
66
+
67
+ # Check if tool implements an interface
68
+ #
69
+ # @param interface_name [String, Symbol] the interface name
70
+ # @return [Boolean] true if implements
71
+ def implements?(interface_name)
72
+ implements == interface_name.to_s
73
+ end
74
+
75
+ # Check if tool is available on a platform
76
+ #
77
+ # @param platform [Symbol] the platform
78
+ # @return [Boolean] true if available
79
+ def available_on?(platform)
80
+ return true if profiles.empty?
81
+
82
+ profiles.any? { |p| p.is_a?(PlatformProfile) && p.supports_platform?(platform) }
83
+ end
84
+
85
+ # Resolve profile inheritance
86
+ #
87
+ # Merges parent profile commands into child profiles that have `inherits` set.
88
+ # The child profile's commands take precedence over parent commands.
89
+ #
90
+ # @return [self] returns self for chaining
91
+ def resolve_inheritance!
92
+ return self unless profiles
93
+
94
+ profiles.each do |profile|
95
+ next unless profile.inherits
96
+
97
+ # Find parent profile by name
98
+ parent_profile = profiles.find { |p| p.name == profile.inherits }
99
+ next unless parent_profile
100
+
101
+ # Merge parent commands into child (child takes precedence)
102
+ parent_commands = parent_profile.commands || []
103
+ child_commands = profile.commands || []
104
+
105
+ # Create a map of child commands by name for quick lookup
106
+ child_commands_map = child_commands.to_h { |c| [c.name, c] }
107
+
108
+ # Add parent commands that don't exist in child
109
+ merged_commands = child_commands.dup
110
+ parent_commands.each do |parent_cmd|
111
+ merged_commands << parent_cmd unless child_commands_map.key?(parent_cmd.name)
112
+ end
113
+
114
+ # Update profile commands and clear index so it rebuilds on next access
115
+ profile.commands = merged_commands
116
+ profile.clear_commands_index!
117
+ end
118
+
119
+ self
120
+ end
121
+
122
+ # Get the schema version
123
+ #
124
+ # @return [String, nil] the schema version (e.g., "1.0", "1.1", "1.2")
125
+ def schema_version
126
+ ukiryu_schema
127
+ end
128
+
129
+ # Get the self URI
130
+ #
131
+ # @return [String, nil] the self URI
132
+ def self_uri
133
+ @self_uri
134
+ end
135
+
136
+ # Check if a specific schema version is specified
137
+ #
138
+ # @param version [String] the version to check
139
+ # @return [Boolean] true if this is the schema version
140
+ def schema_version?(version)
141
+ ukiryu_schema == version
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+
5
+ module Ukiryu
6
+ class ToolMetadata
7
+ # Lightweight metadata model for tools
8
+ # Contains only the essential information needed for tool discovery
9
+ # without loading the full profile definition
10
+
11
+ attr_reader :name, :version, :display_name, :implements, :homepage,
12
+ :description, :aliases, :tool_name, :registry_path, :default_command
13
+
14
+ def initialize(name:, version:, display_name: nil, implements: nil,
15
+ homepage: nil, description: nil, aliases: nil,
16
+ tool_name: nil, registry_path: nil, default_command: nil)
17
+ @name = name
18
+ @version = version
19
+ @display_name = display_name
20
+ @implements = implements
21
+ @homepage = homepage
22
+ @description = description
23
+ @aliases = Array(aliases || [])
24
+ @tool_name = tool_name || name
25
+ @registry_path = registry_path
26
+ @default_command = default_command
27
+ end
28
+
29
+ # Check if this metadata matches an interface
30
+ #
31
+ # @param interface_name [Symbol, String] the interface to check
32
+ # @return [Boolean] true if this tool implements the interface
33
+ def implements?(interface_name)
34
+ @implements == interface_name.to_sym
35
+ end
36
+
37
+ # Get the primary command name for this tool
38
+ # Returns the default_command from YAML if set, otherwise the implements value,
39
+ # otherwise falls back to the tool name
40
+ #
41
+ # @return [Symbol, nil] the default command name
42
+ def default_command
43
+ @default_command || @implements || @name.to_sym
44
+ end
45
+
46
+ # String representation
47
+ #
48
+ # @return [String] description string
49
+ def to_s
50
+ "#{@display_name || @name} v#{@version}"
51
+ end
52
+
53
+ # Inspect
54
+ #
55
+ # @return [String] inspection string
56
+ def inspect
57
+ "#<#{self.class.name} name=#{@name.inspect} version=#{@version.inspect} implements=#{@implements.inspect}>"
58
+ end
59
+
60
+ # Class method to create from YAML hash
61
+ # Extracts only metadata fields from a full tool profile
62
+ #
63
+ # @param hash [Hash] the YAML profile hash
64
+ # @param tool_name [String] the tool name
65
+ # @param registry_path [String] the registry path
66
+ # @return [ToolMetadata] the metadata object
67
+ def self.from_hash(hash, tool_name:, registry_path: nil)
68
+ new(
69
+ name: tool_name,
70
+ version: hash['version'],
71
+ display_name: hash['display_name'],
72
+ implements: hash['implements']&.to_sym,
73
+ homepage: hash['homepage'],
74
+ description: hash['description'],
75
+ aliases: hash['aliases'],
76
+ default_command: hash['default_command'],
77
+ tool_name: tool_name,
78
+ registry_path: registry_path
79
+ )
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ukiryu
4
+ module Models
5
+ # Result of validating a tool profile against the schema
6
+ #
7
+ # Contains validation status and any errors found during validation.
8
+ class ValidationResult
9
+ attr_reader :tool_name, :valid, :errors
10
+
11
+ # Create a new validation result
12
+ #
13
+ # @param tool_name [String] the tool name that was validated
14
+ # @param valid [Boolean] whether validation passed
15
+ # @param errors [Array<String>] list of validation errors
16
+ def initialize(tool_name:, valid:, errors: [])
17
+ @tool_name = tool_name
18
+ @valid = valid
19
+ @errors = errors
20
+ end
21
+
22
+ # Create a valid result (no errors)
23
+ #
24
+ # @param tool_name [String] the tool name
25
+ # @return [ValidationResult] a valid result
26
+ def self.valid(tool_name)
27
+ new(tool_name: tool_name, valid: true, errors: [])
28
+ end
29
+
30
+ # Create an invalid result with errors
31
+ #
32
+ # @param tool_name [String] the tool name
33
+ # @param errors [Array<String>] list of validation errors
34
+ # @return [ValidationResult] an invalid result
35
+ def self.invalid(tool_name, errors)
36
+ new(tool_name: tool_name, valid: false, errors: errors)
37
+ end
38
+
39
+ # Create a result for a tool not found
40
+ #
41
+ # @param tool_name [String] the tool name
42
+ # @return [ValidationResult] a not found result
43
+ def self.not_found(tool_name)
44
+ new(tool_name: tool_name, valid: false, errors: ['Tool not found'])
45
+ end
46
+
47
+ # Get a human-readable status message
48
+ #
49
+ # @return [String] status message
50
+ def status_message
51
+ if valid?
52
+ "✓ Valid"
53
+ else
54
+ "✗ Invalid (#{errors.size} error#{errors.size == 1 ? '' : 's'})"
55
+ end
56
+ end
57
+
58
+ # Check if validation passed
59
+ #
60
+ # @return [Boolean] true if valid
61
+ def valid?
62
+ @valid
63
+ end
64
+
65
+ # Check if validation failed
66
+ #
67
+ # @return [Boolean] true if invalid
68
+ def invalid?
69
+ !@valid
70
+ end
71
+
72
+ # Check if tool was not found
73
+ #
74
+ # @return [Boolean] true if tool not found
75
+ def not_found?
76
+ @errors == ['Tool not found']
77
+ end
78
+ end
79
+ end
80
+ end