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.
- checksums.yaml +4 -4
- data/.github/workflows/docs.yml +63 -0
- data/.github/workflows/links.yml +99 -0
- data/.github/workflows/rake.yml +19 -0
- data/.github/workflows/release.yml +27 -0
- data/.gitignore +18 -4
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +213 -0
- data/Gemfile +12 -8
- data/README.adoc +613 -0
- data/Rakefile +2 -2
- data/docs/assets/logo.svg +1 -0
- data/exe/ukiryu +11 -0
- data/lib/ukiryu/action/base.rb +77 -0
- data/lib/ukiryu/cache.rb +199 -0
- data/lib/ukiryu/cli.rb +133 -307
- data/lib/ukiryu/cli_commands/base_command.rb +155 -0
- data/lib/ukiryu/cli_commands/commands_command.rb +120 -0
- data/lib/ukiryu/cli_commands/commands_command.rb.fixed +40 -0
- data/lib/ukiryu/cli_commands/config_command.rb +249 -0
- data/lib/ukiryu/cli_commands/describe_command.rb +326 -0
- data/lib/ukiryu/cli_commands/describe_command.rb.fixed +254 -0
- data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +180 -0
- data/lib/ukiryu/cli_commands/extract_command.rb +84 -0
- data/lib/ukiryu/cli_commands/info_command.rb +156 -0
- data/lib/ukiryu/cli_commands/list_command.rb +70 -0
- data/lib/ukiryu/cli_commands/opts_command.rb +106 -0
- data/lib/ukiryu/cli_commands/opts_command.rb.fixed +105 -0
- data/lib/ukiryu/cli_commands/response_formatter.rb +240 -0
- data/lib/ukiryu/cli_commands/run_command.rb +375 -0
- data/lib/ukiryu/cli_commands/run_file_command.rb +215 -0
- data/lib/ukiryu/cli_commands/system_command.rb +90 -0
- data/lib/ukiryu/cli_commands/validate_command.rb +87 -0
- data/lib/ukiryu/cli_commands/version_command.rb +16 -0
- data/lib/ukiryu/cli_commands/which_command.rb +166 -0
- data/lib/ukiryu/command_builder.rb +205 -0
- data/lib/ukiryu/config/env_provider.rb +64 -0
- data/lib/ukiryu/config/env_schema.rb +63 -0
- data/lib/ukiryu/config/override_resolver.rb +68 -0
- data/lib/ukiryu/config/type_converter.rb +59 -0
- data/lib/ukiryu/config.rb +249 -0
- data/lib/ukiryu/errors.rb +3 -0
- data/lib/ukiryu/executable_locator.rb +114 -0
- data/lib/ukiryu/execution/command_info.rb +64 -0
- data/lib/ukiryu/execution/metadata.rb +97 -0
- data/lib/ukiryu/execution/output.rb +144 -0
- data/lib/ukiryu/execution/result.rb +194 -0
- data/lib/ukiryu/execution.rb +15 -0
- data/lib/ukiryu/execution_context.rb +251 -0
- data/lib/ukiryu/executor.rb +76 -493
- data/lib/ukiryu/extractors/base_extractor.rb +63 -0
- data/lib/ukiryu/extractors/extractor.rb +150 -0
- data/lib/ukiryu/extractors/help_parser.rb +188 -0
- data/lib/ukiryu/extractors/native_extractor.rb +47 -0
- data/lib/ukiryu/io.rb +196 -0
- data/lib/ukiryu/logger.rb +544 -0
- data/lib/ukiryu/models/argument.rb +28 -0
- data/lib/ukiryu/models/argument_definition.rb +119 -0
- data/lib/ukiryu/models/arguments.rb +113 -0
- data/lib/ukiryu/models/command_definition.rb +176 -0
- data/lib/ukiryu/models/command_info.rb +37 -0
- data/lib/ukiryu/models/components.rb +107 -0
- data/lib/ukiryu/models/env_var_definition.rb +30 -0
- data/lib/ukiryu/models/error_response.rb +41 -0
- data/lib/ukiryu/models/execution_metadata.rb +31 -0
- data/lib/ukiryu/models/execution_report.rb +236 -0
- data/lib/ukiryu/models/exit_codes.rb +74 -0
- data/lib/ukiryu/models/flag_definition.rb +67 -0
- data/lib/ukiryu/models/option_definition.rb +102 -0
- data/lib/ukiryu/models/output_info.rb +25 -0
- data/lib/ukiryu/models/platform_profile.rb +153 -0
- data/lib/ukiryu/models/routing.rb +211 -0
- data/lib/ukiryu/models/search_paths.rb +39 -0
- data/lib/ukiryu/models/success_response.rb +85 -0
- data/lib/ukiryu/models/tool_definition.rb +145 -0
- data/lib/ukiryu/models/tool_metadata.rb +82 -0
- data/lib/ukiryu/models/validation_result.rb +80 -0
- data/lib/ukiryu/models/version_compatibility.rb +152 -0
- data/lib/ukiryu/models/version_detection.rb +39 -0
- data/lib/ukiryu/models.rb +23 -0
- data/lib/ukiryu/options/base.rb +95 -0
- data/lib/ukiryu/options_builder/formatter.rb +87 -0
- data/lib/ukiryu/options_builder/validator.rb +43 -0
- data/lib/ukiryu/options_builder.rb +311 -0
- data/lib/ukiryu/platform.rb +6 -6
- data/lib/ukiryu/registry.rb +143 -183
- data/lib/ukiryu/response/base.rb +217 -0
- data/lib/ukiryu/runtime.rb +179 -0
- data/lib/ukiryu/schema_validator.rb +8 -10
- data/lib/ukiryu/shell/bash.rb +3 -3
- data/lib/ukiryu/shell/cmd.rb +4 -4
- data/lib/ukiryu/shell/fish.rb +1 -1
- data/lib/ukiryu/shell/powershell.rb +3 -3
- data/lib/ukiryu/shell/sh.rb +1 -1
- data/lib/ukiryu/shell/zsh.rb +1 -1
- data/lib/ukiryu/shell.rb +146 -39
- data/lib/ukiryu/thor_ext.rb +208 -0
- data/lib/ukiryu/tool.rb +649 -258
- data/lib/ukiryu/tool_index.rb +224 -0
- data/lib/ukiryu/tools/base.rb +381 -0
- data/lib/ukiryu/tools/class_generator.rb +132 -0
- data/lib/ukiryu/tools/executable_finder.rb +29 -0
- data/lib/ukiryu/tools/generator.rb +154 -0
- data/lib/ukiryu/tools.rb +109 -0
- data/lib/ukiryu/type.rb +28 -43
- data/lib/ukiryu/validation/constraints.rb +281 -0
- data/lib/ukiryu/validation/validator.rb +188 -0
- data/lib/ukiryu/validation.rb +21 -0
- data/lib/ukiryu/version.rb +1 -1
- data/lib/ukiryu/version_detector.rb +51 -0
- data/lib/ukiryu.rb +31 -15
- data/ukiryu-proposal.md +2952 -0
- data/ukiryu.gemspec +18 -14
- metadata +137 -5
- 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
|