ukiryu 0.1.3 → 0.1.4
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/README.adoc +21 -19
- data/lib/ukiryu/definition/definition_validator.rb +1 -66
- data/lib/ukiryu/definition/discovery.rb +1 -1
- data/lib/ukiryu/executable_locator.rb +8 -1
- data/lib/ukiryu/executor.rb +7 -0
- data/lib/ukiryu/man_page_parser.rb +179 -0
- data/lib/ukiryu/models/version_detection.rb +27 -0
- data/lib/ukiryu/models/version_info.rb +98 -0
- data/lib/ukiryu/register.rb +2 -2
- data/lib/ukiryu/thor_ext.rb +1 -1
- data/lib/ukiryu/tool.rb +80 -5
- data/lib/ukiryu/tool_index.rb +2 -2
- data/lib/ukiryu/version.rb +1 -1
- data/lib/ukiryu/version_detector.rb +108 -11
- data/ukiryu.gemspec +1 -1
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 72da5a25186dfa4f3b3b53d0503c46f76086aaa94e219523126f331f10ca88d0
|
|
4
|
+
data.tar.gz: 6fcd4fa7a7f1af0830a1d2ea75632f63ff5b8b1333bfc4ce6b565444cc263898
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 264926ef984787a01fd48941daea277027fa14ae405c63e15b765464338e474ef78c7f3d98a81f41762a281b140139bf9c8d515e468bf3350b584476e5118db0
|
|
7
|
+
data.tar.gz: aa91036800ea42466283e46e05fffc47179cffe08d9340f246f5abb44d3bf9276742804ba61c8ad3e00acb384f9d5ef56b55a3e87cc2990e675f50811a1ab5ec
|
data/README.adoc
CHANGED
|
@@ -83,7 +83,7 @@ Without Ukiryu, CLI tools suffer from:
|
|
|
83
83
|
=== The Ukiryu Solution
|
|
84
84
|
|
|
85
85
|
[cols="1,1,4"]
|
|
86
|
-
|
|
86
|
+
|===
|
|
87
87
|
|Problem |Traditional CLI |Ukiryu Solution
|
|
88
88
|
|
|
89
89
|
|Argument fragility
|
|
@@ -107,18 +107,19 @@ Without Ukiryu, CLI tools suffer from:
|
|
|
107
107
|
|
|
108
108
|
|Discovery friction
|
|
109
109
|
|`tool --help` |`ukiryu describe tool command`
|
|
110
|
+
|
|
110
111
|
|===
|
|
111
112
|
|
|
112
113
|
=== Why "OpenAPI for CLIs"?
|
|
113
114
|
|
|
114
|
-
OpenAPI
|
|
115
|
+
OpenAPI provides the following benefits for REST APIs by:
|
|
115
116
|
|
|
116
117
|
1. **Standardizing** - Machine-readable schema definitions
|
|
117
118
|
2. **Documenting** - Auto-generated interactive documentation
|
|
118
119
|
3. **Type-Safe** - Request/response validation
|
|
119
120
|
4. **Versioning** - Multiple API versions coexisting
|
|
120
121
|
|
|
121
|
-
Ukiryu
|
|
122
|
+
Ukiryu provides the same benefits for CLI tools by:
|
|
122
123
|
|
|
123
124
|
* **Schema**: YAML profiles replace grepping --help
|
|
124
125
|
* **Docs**: `ukiryu describe` replaces man pages
|
|
@@ -136,7 +137,8 @@ Ukiryu brings the same revolution to CLI tools:
|
|
|
136
137
|
// What is Ukiryu?
|
|
137
138
|
== What is Ukiryu?
|
|
138
139
|
|
|
139
|
-
Ukiryu is a framework that turns CLI tools into well-defined, versioned APIs
|
|
140
|
+
Ukiryu is a framework that turns CLI tools into well-defined, versioned APIs
|
|
141
|
+
through declarative YAML profiles.
|
|
140
142
|
|
|
141
143
|
=== Key Concepts
|
|
142
144
|
|
|
@@ -150,32 +152,32 @@ Ukiryu is a framework that turns CLI tools into well-defined, versioned APIs thr
|
|
|
150
152
|
|
|
151
153
|
[source]
|
|
152
154
|
----
|
|
153
|
-
|
|
154
|
-
│
|
|
155
|
+
┌───────────────────────────────────────────────────────────────────┐
|
|
156
|
+
│ │
|
|
155
157
|
│ User Code (Ruby / CLI) │
|
|
156
158
|
│ ├─ tool.execute(:inkscape, { inputs: [...] }) │
|
|
157
159
|
│ └─ ukiryu exec inkscape export inputs=... │
|
|
158
|
-
│
|
|
160
|
+
│ │
|
|
159
161
|
└─────────────────────────────┬─────────────────────────────────────┘
|
|
160
162
|
│
|
|
161
163
|
▼
|
|
162
164
|
┌─────────────────────────────▼─────────────────────────────────────┐
|
|
163
|
-
│ Ukiryu Framework
|
|
164
|
-
│ ├─ Register (loads YAML profiles)
|
|
165
|
-
│ ├─ Tool (selects profile, detects version)
|
|
166
|
-
│ ├─ CommandBuilder (formats arguments for shell)
|
|
167
|
-
│ ├─ Executor (runs commands, captures output)
|
|
168
|
-
│ └─ Shell Layer (platform-specific quoting/escaping)
|
|
169
|
-
│
|
|
165
|
+
│ Ukiryu Framework │
|
|
166
|
+
│ ├─ Register (loads YAML profiles) │
|
|
167
|
+
│ ├─ Tool (selects profile, detects version) │
|
|
168
|
+
│ ├─ CommandBuilder (formats arguments for shell) │
|
|
169
|
+
│ ├─ Executor (runs commands, captures output) │
|
|
170
|
+
│ └─ Shell Layer (platform-specific quoting/escaping) │
|
|
171
|
+
│ │
|
|
170
172
|
└─────────────────────────────┬─────────────────────────────────────┘
|
|
171
173
|
│
|
|
172
174
|
▼
|
|
173
175
|
┌─────────────────────────────▼─────────────────────────────────────┐
|
|
174
|
-
│ YAML Tool Profiles (Declarative API Definition)
|
|
175
|
-
│ ├─ tools/inkscape/1.0.yaml (Modern Inkscape)
|
|
176
|
-
│ ├─ tools/inkscape/0.92.yaml (Legacy Inkscape)
|
|
177
|
-
│ └─ tools/imagemagick/7.1.yaml (ImageMagick 7.1)
|
|
178
|
-
|
|
176
|
+
│ YAML Tool Profiles (Declarative API Definition) │
|
|
177
|
+
│ ├─ tools/inkscape/1.0.yaml (Modern Inkscape) │
|
|
178
|
+
│ ├─ tools/inkscape/0.92.yaml (Legacy Inkscape) │
|
|
179
|
+
│ └─ tools/imagemagick/7.1.yaml (ImageMagick 7.1) │
|
|
180
|
+
└───────────────────────────────────────────────────────────────────┘
|
|
179
181
|
----
|
|
180
182
|
|
|
181
183
|
=== Tool Profile Schema
|
|
@@ -73,12 +73,6 @@ module Ukiryu
|
|
|
73
73
|
errors = []
|
|
74
74
|
warnings = []
|
|
75
75
|
|
|
76
|
-
# Debug: Check which tool is being validated
|
|
77
|
-
if ENV['DEBUG_SCHEMA_VALIDATION']
|
|
78
|
-
tool_name = definition['name'] || definition[:name] || 'unknown'
|
|
79
|
-
puts "DEBUG: Validating tool: #{tool_name}"
|
|
80
|
-
end
|
|
81
|
-
|
|
82
76
|
# Basic structural validation (always available)
|
|
83
77
|
structural_result = validate_structure(definition)
|
|
84
78
|
errors.concat(structural_result[:errors])
|
|
@@ -88,9 +82,6 @@ module Ukiryu
|
|
|
88
82
|
if schema_validation_available?
|
|
89
83
|
schema = schema_path ? load_schema(schema_path) : find_and_load_schema
|
|
90
84
|
if schema
|
|
91
|
-
if ENV['DEBUG_SCHEMA_VALIDATION']
|
|
92
|
-
puts "DEBUG: Schema loaded, proceeding with JSON Schema validation"
|
|
93
|
-
end
|
|
94
85
|
schema_result = validate_against_schema(definition, schema)
|
|
95
86
|
errors.concat(schema_result[:errors])
|
|
96
87
|
warnings.concat(schema_result[:warnings])
|
|
@@ -221,65 +212,9 @@ module Ukiryu
|
|
|
221
212
|
warnings = []
|
|
222
213
|
|
|
223
214
|
begin
|
|
224
|
-
# Debug: Check data BEFORE stringify_keys
|
|
225
|
-
if ENV['DEBUG_SCHEMA_VALIDATION']
|
|
226
|
-
tool_name = definition['name'] || definition[:name] || 'unknown'
|
|
227
|
-
puts "DEBUG: validate_against_schema for tool: #{tool_name}"
|
|
228
|
-
|
|
229
|
-
# Check flags[0] before stringify
|
|
230
|
-
if definition['profiles'] && definition['profiles'][0] &&
|
|
231
|
-
definition['profiles'][0]['commands'] &&
|
|
232
|
-
definition['profiles'][0]['commands'][0] &&
|
|
233
|
-
definition['profiles'][0]['commands'][0]['flags'] &&
|
|
234
|
-
definition['profiles'][0]['commands'][0]['flags'][0]
|
|
235
|
-
flg0_before = definition['profiles'][0]['commands'][0]['flags'][0]
|
|
236
|
-
puts "DEBUG: Flag 0 BEFORE stringify: #{flg0_before.inspect}"
|
|
237
|
-
puts "DEBUG: Flag 0 name BEFORE stringify: #{flg0_before['name'].inspect}"
|
|
238
|
-
puts "DEBUG: Flag 0 name class BEFORE stringify: #{flg0_before['name'].class}"
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
|
|
242
215
|
# Convert symbol keys to strings for JSON Schema validation
|
|
243
216
|
stringified = stringify_keys(definition)
|
|
244
|
-
|
|
245
|
-
if ENV['DEBUG_SCHEMA_VALIDATION']
|
|
246
|
-
puts "DEBUG: After stringify_keys, checking keys..."
|
|
247
|
-
puts "DEBUG: Top-level keys: #{stringified.keys.inspect}"
|
|
248
|
-
puts "DEBUG: Top-level key classes: #{stringified.keys.map(&:class).inspect}"
|
|
249
|
-
|
|
250
|
-
# Check options[0] and flags[0] if they exist
|
|
251
|
-
if stringified['profiles'] && stringified['profiles'][0] &&
|
|
252
|
-
stringified['profiles'][0]['commands'] &&
|
|
253
|
-
stringified['profiles'][0]['commands'][0]
|
|
254
|
-
cmd = stringified['profiles'][0]['commands'][0]
|
|
255
|
-
|
|
256
|
-
# Check options[0]
|
|
257
|
-
if cmd['options'] && cmd['options'][0]
|
|
258
|
-
opt0 = cmd['options'][0]
|
|
259
|
-
puts "DEBUG: Option 0 after stringify: #{opt0.inspect}"
|
|
260
|
-
puts "DEBUG: Option 0 keys: #{opt0.keys.inspect}"
|
|
261
|
-
puts "DEBUG: Option 0 name: #{opt0['name'].inspect}"
|
|
262
|
-
puts "DEBUG: Option 0 name class: #{opt0['name'].class}"
|
|
263
|
-
end
|
|
264
|
-
|
|
265
|
-
# Check flags[0]
|
|
266
|
-
if cmd['flags'] && cmd['flags'][0]
|
|
267
|
-
flg0 = cmd['flags'][0]
|
|
268
|
-
puts "DEBUG: Flag 0 after stringify: #{flg0.inspect}"
|
|
269
|
-
puts "DEBUG: Flag 0 keys: #{flg0.keys.inspect}"
|
|
270
|
-
puts "DEBUG: Flag 0 name: #{flg0['name'].inspect}"
|
|
271
|
-
puts "DEBUG: Flag 0 name class: #{flg0['name'].class}"
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
# Also check options[26] for grep
|
|
275
|
-
if cmd['options'] && cmd['options'][26]
|
|
276
|
-
opt26 = cmd['options'][26]
|
|
277
|
-
puts "DEBUG: Option 26 after stringify: #{opt26.inspect}"
|
|
278
|
-
puts "DEBUG: Option 26 name: #{opt26['name'].inspect}"
|
|
279
|
-
end
|
|
280
|
-
end
|
|
281
|
-
puts "DEBUG: JSON::Schema version: #{JSON::Schema::VERSION rescue 'unknown'}"
|
|
282
|
-
end
|
|
217
|
+
|
|
283
218
|
validation = JSON::Validator.fully_validate(schema, stringified, errors_as_objects: true)
|
|
284
219
|
|
|
285
220
|
validation.each do |error|
|
|
@@ -217,7 +217,7 @@ module Ukiryu
|
|
|
217
217
|
def self.metadata_from_file(yaml_file, source_type)
|
|
218
218
|
# Try to load just the name and version from YAML
|
|
219
219
|
yaml_content = File.read(yaml_file)
|
|
220
|
-
data = YAML.safe_load(yaml_content, permitted_classes: [Symbol])
|
|
220
|
+
data = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true)
|
|
221
221
|
|
|
222
222
|
return nil unless data.is_a?(Hash)
|
|
223
223
|
return nil unless data['name']
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative 'executor'
|
|
4
4
|
require_relative 'platform'
|
|
5
|
+
require_relative 'models/search_paths'
|
|
5
6
|
|
|
6
7
|
module Ukiryu
|
|
7
8
|
# Executable locator for finding tool executables
|
|
@@ -74,7 +75,7 @@ module Ukiryu
|
|
|
74
75
|
|
|
75
76
|
# Normalize search paths to array format
|
|
76
77
|
#
|
|
77
|
-
# @param search_paths [Array<String>, Models::SearchPaths] the search paths
|
|
78
|
+
# @param search_paths [Array<String>, Models::SearchPaths, Hash] the search paths
|
|
78
79
|
# @param platform [Symbol] the platform
|
|
79
80
|
# @return [Array<String>] normalized array of paths
|
|
80
81
|
def normalize_search_paths(search_paths, platform)
|
|
@@ -83,6 +84,12 @@ module Ukiryu
|
|
|
83
84
|
# If it's a SearchPaths model, get platform-specific paths
|
|
84
85
|
return search_paths.for_platform(platform) || [] if search_paths.is_a?(Models::SearchPaths)
|
|
85
86
|
|
|
87
|
+
# If it's a Hash, extract platform-specific paths
|
|
88
|
+
if search_paths.is_a?(Hash)
|
|
89
|
+
platform_paths = search_paths[platform] || search_paths[platform.to_s]
|
|
90
|
+
return platform_paths || [] if platform_paths
|
|
91
|
+
end
|
|
92
|
+
|
|
86
93
|
# Already an array
|
|
87
94
|
search_paths
|
|
88
95
|
end
|
data/lib/ukiryu/executor.rb
CHANGED
|
@@ -45,6 +45,11 @@ module Ukiryu
|
|
|
45
45
|
cwd = options[:cwd]
|
|
46
46
|
stdin = options[:stdin]
|
|
47
47
|
|
|
48
|
+
# Suppress thread warnings from Open3 (cosmetic IOError from stream closure)
|
|
49
|
+
# Open3's internal threads may raise IOError when streams close early
|
|
50
|
+
original_setting = Thread.report_on_exception
|
|
51
|
+
Thread.report_on_exception = false
|
|
52
|
+
|
|
48
53
|
started_at = Time.now
|
|
49
54
|
begin
|
|
50
55
|
result = if stdin
|
|
@@ -55,6 +60,8 @@ module Ukiryu
|
|
|
55
60
|
rescue Timeout::Error
|
|
56
61
|
Time.now
|
|
57
62
|
raise TimeoutError, "Command timed out after #{timeout} seconds: #{executable}"
|
|
63
|
+
ensure
|
|
64
|
+
Thread.report_on_exception = original_setting
|
|
58
65
|
end
|
|
59
66
|
finished_at = Time.now
|
|
60
67
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'platform'
|
|
4
|
+
require_relative 'executor'
|
|
5
|
+
require 'time'
|
|
6
|
+
require 'date'
|
|
7
|
+
|
|
8
|
+
module Ukiryu
|
|
9
|
+
# Man page date parser for extracting version information
|
|
10
|
+
#
|
|
11
|
+
# Parses man page `.Dd` macros to extract dates as version information.
|
|
12
|
+
# Used as fallback for system tools that don't support --version flags.
|
|
13
|
+
#
|
|
14
|
+
# @example Parse man page date
|
|
15
|
+
# date = ManPageParser.parse_date('/usr/share/man/man1/xargs.1')
|
|
16
|
+
# # => "2020-09-21"
|
|
17
|
+
module ManPageParser
|
|
18
|
+
class << self
|
|
19
|
+
# Parse date from man page
|
|
20
|
+
#
|
|
21
|
+
# Extracts the `.Dd` (date) macro from a man page and converts it to
|
|
22
|
+
# ISO 8601 format (YYYY-MM-DD).
|
|
23
|
+
#
|
|
24
|
+
# @param man_page_path [String] path to the man page file
|
|
25
|
+
# @return [String, nil] ISO 8601 date string or nil if not found
|
|
26
|
+
def parse_date(man_page_path)
|
|
27
|
+
return nil unless man_page_path
|
|
28
|
+
return nil unless File.exist?(man_page_path)
|
|
29
|
+
|
|
30
|
+
content = File.read(man_page_path, encoding: 'UTF-8')
|
|
31
|
+
|
|
32
|
+
# Find .Dd line
|
|
33
|
+
# Format: .Dd Month DD, YYYY or .Dd DD Month YYYY
|
|
34
|
+
dd_line = content.lines.find { |line| line =~ /^\.Dd\s+/ }
|
|
35
|
+
|
|
36
|
+
return nil unless dd_line
|
|
37
|
+
|
|
38
|
+
# Extract and parse date
|
|
39
|
+
extract_date_from_dd_line(dd_line)
|
|
40
|
+
rescue Errno::ENOENT, Errno::EACCES => e
|
|
41
|
+
# File not found or inaccessible - silent failure
|
|
42
|
+
nil
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get man page path for a tool on current platform
|
|
46
|
+
#
|
|
47
|
+
# @param tool_name [String] the tool name
|
|
48
|
+
# @param paths [Hash] platform-specific path templates
|
|
49
|
+
# @return [String, nil] resolved man page path or nil
|
|
50
|
+
def resolve_man_page_path(tool_name, paths = {})
|
|
51
|
+
platform = Platform.current
|
|
52
|
+
|
|
53
|
+
# Get platform-specific path pattern
|
|
54
|
+
path_pattern = paths[platform]
|
|
55
|
+
|
|
56
|
+
return nil unless path_pattern
|
|
57
|
+
|
|
58
|
+
# Expand tool name placeholder if present
|
|
59
|
+
path_pattern.gsub('{tool}', tool_name)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Parse date from multiple possible man page locations
|
|
63
|
+
#
|
|
64
|
+
# Tries multiple paths in order and returns the first successfully
|
|
65
|
+
# parsed date.
|
|
66
|
+
#
|
|
67
|
+
# @param possible_paths [Array<String>] list of possible man page paths
|
|
68
|
+
# @return [String, nil] ISO 8601 date string or nil if none found
|
|
69
|
+
def parse_from_fallback(possible_paths)
|
|
70
|
+
possible_paths.each do |path|
|
|
71
|
+
date = parse_date(path)
|
|
72
|
+
return date if date
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
# Extract date from .Dd line
|
|
81
|
+
#
|
|
82
|
+
# Handles various man page date formats:
|
|
83
|
+
# - .Dd September 21, 2020
|
|
84
|
+
# - .Dd 21 September 2020
|
|
85
|
+
# - .Dd 2020-09-21
|
|
86
|
+
#
|
|
87
|
+
# @param dd_line [String] the .Dd line
|
|
88
|
+
# @return [String, nil] ISO 8601 date or nil
|
|
89
|
+
def extract_date_from_dd_line(dd_line)
|
|
90
|
+
# Remove .Dd prefix
|
|
91
|
+
date_str = dd_line.sub(/^\.Dd\s+/, '').strip
|
|
92
|
+
|
|
93
|
+
# Try parsing different formats
|
|
94
|
+
parsed = try_parse_date_formats(date_str)
|
|
95
|
+
|
|
96
|
+
parsed&.strftime('%Y-%m-%d')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Try parsing various date formats
|
|
100
|
+
#
|
|
101
|
+
# @param date_str [String] the date string
|
|
102
|
+
# @return [Date, nil] parsed date or nil
|
|
103
|
+
def try_parse_date_formats(date_str)
|
|
104
|
+
# Format: "September 21, 2020" or "21 September 2020"
|
|
105
|
+
# Parse with Date.parse which handles many formats
|
|
106
|
+
Date.parse(date_str)
|
|
107
|
+
rescue ArgumentError, Date::Error
|
|
108
|
+
# Try regex-based extraction
|
|
109
|
+
extract_date_with_regex(date_str)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Extract date using regex patterns
|
|
113
|
+
#
|
|
114
|
+
# @param date_str [String] the date string
|
|
115
|
+
# @return [Date, nil] parsed date or nil
|
|
116
|
+
def extract_date_with_regex(date_str)
|
|
117
|
+
# Match "Month DD, YYYY" or "DD Month YYYY" or "YYYY-MM-DD"
|
|
118
|
+
patterns = [
|
|
119
|
+
/(\w+)\s+(\d+),?\s+(\d{4})/, # Month DD, YYYY
|
|
120
|
+
/(\d+)\s+(\w+)\s+(\d{4})/, # DD Month YYYY
|
|
121
|
+
/(\d{4})-(\d{2})-(\d{2})/ # YYYY-MM-DD
|
|
122
|
+
]
|
|
123
|
+
|
|
124
|
+
patterns.each do |pattern|
|
|
125
|
+
match = date_str.match(pattern)
|
|
126
|
+
next unless match
|
|
127
|
+
|
|
128
|
+
captures = match.captures
|
|
129
|
+
|
|
130
|
+
# Determine order based on capture count
|
|
131
|
+
if captures[0]&.length == 4 # First capture is year (YYYY-MM-DD)
|
|
132
|
+
year, month, day = captures
|
|
133
|
+
return Date.new(year.to_i, month.to_i, day.to_i)
|
|
134
|
+
elsif captures[2]&.length == 4 # Last capture is year
|
|
135
|
+
month_or_day, day_or_month, year = captures
|
|
136
|
+
|
|
137
|
+
# Check if first is month name or number
|
|
138
|
+
if month_or_day =~ /^\d+$/
|
|
139
|
+
# DD Month YYYY format
|
|
140
|
+
day, month_name, year = captures
|
|
141
|
+
month = month_name_to_number(month_name)
|
|
142
|
+
return Date.new(year.to_i, month, day.to_i) if month
|
|
143
|
+
else
|
|
144
|
+
# Month DD, YYYY format
|
|
145
|
+
month_name, day, year = captures
|
|
146
|
+
month = month_name_to_number(month_name)
|
|
147
|
+
return Date.new(year.to_i, month, day.to_i) if month
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
nil
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Convert month name to number
|
|
156
|
+
#
|
|
157
|
+
# @param month_name [String] the month name
|
|
158
|
+
# @return [Integer, nil] month number (1-12) or nil
|
|
159
|
+
def month_name_to_number(month_name)
|
|
160
|
+
months = {
|
|
161
|
+
'january' => 1, 'jan' => 1,
|
|
162
|
+
'february' => 2, 'feb' => 2,
|
|
163
|
+
'march' => 3, 'mar' => 3,
|
|
164
|
+
'april' => 4, 'apr' => 4,
|
|
165
|
+
'may' => 5,
|
|
166
|
+
'june' => 6, 'jun' => 6,
|
|
167
|
+
'july' => 7, 'jul' => 7,
|
|
168
|
+
'august' => 8, 'aug' => 8,
|
|
169
|
+
'september' => 9, 'sep' => 9, 'sept' => 9,
|
|
170
|
+
'october' => 10, 'oct' => 10,
|
|
171
|
+
'november' => 11, 'nov' => 11,
|
|
172
|
+
'december' => 12, 'dec' => 12
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
months[month_name.downcase]
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -4,6 +4,23 @@ require 'lutaml/model'
|
|
|
4
4
|
|
|
5
5
|
module Ukiryu
|
|
6
6
|
module Models
|
|
7
|
+
# Version detection method
|
|
8
|
+
#
|
|
9
|
+
# Single detection method in the fallback hierarchy
|
|
10
|
+
class VersionDetectionMethod < Lutaml::Model::Serializable
|
|
11
|
+
attribute :type, :string # 'command' or 'man_page'
|
|
12
|
+
attribute :command, :string
|
|
13
|
+
attribute :pattern, :string
|
|
14
|
+
attribute :paths, :hash # Platform-specific paths for man_page
|
|
15
|
+
|
|
16
|
+
yaml do
|
|
17
|
+
map_element 'type', to: :type
|
|
18
|
+
map_element 'command', to: :command
|
|
19
|
+
map_element 'pattern', to: :pattern
|
|
20
|
+
map_element 'paths', to: :paths
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
7
24
|
# Version detection configuration
|
|
8
25
|
#
|
|
9
26
|
# @example Command-based version detection (GNU tools)
|
|
@@ -19,17 +36,27 @@ module Ukiryu
|
|
|
19
36
|
# pattern: 'macOS ([\d.]+)',
|
|
20
37
|
# source: 'man'
|
|
21
38
|
# )
|
|
39
|
+
#
|
|
40
|
+
# @example Fallback hierarchy with detection_methods array
|
|
41
|
+
# vd = VersionDetection.new(
|
|
42
|
+
# detection_methods: [
|
|
43
|
+
# VersionDetectionMethod.new(type: 'command', command: '--version', pattern: '(\d+\.\d+)'),
|
|
44
|
+
# VersionDetectionMethod.new(type: 'man_page', paths: { macos: '/usr/share/man/man1/xargs.1' })
|
|
45
|
+
# ]
|
|
46
|
+
# )
|
|
22
47
|
class VersionDetection < Lutaml::Model::Serializable
|
|
23
48
|
attribute :command, :string, collection: true, default: []
|
|
24
49
|
attribute :pattern, :string
|
|
25
50
|
attribute :modern_threshold, :string
|
|
26
51
|
attribute :source, :string, default: 'command' # 'command' or 'man'
|
|
52
|
+
attribute :detection_methods, VersionDetectionMethod, collection: true, default: []
|
|
27
53
|
|
|
28
54
|
yaml do
|
|
29
55
|
map_element 'command', to: :command
|
|
30
56
|
map_element 'pattern', to: :pattern
|
|
31
57
|
map_element 'modern_threshold', to: :modern_threshold
|
|
32
58
|
map_element 'source', to: :source
|
|
59
|
+
map_element 'detection_methods', to: :detection_methods
|
|
33
60
|
end
|
|
34
61
|
|
|
35
62
|
# Hash-like access for Base.detect_version compatibility
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ukiryu
|
|
4
|
+
module Models
|
|
5
|
+
# Version information with detection method tracking
|
|
6
|
+
#
|
|
7
|
+
# Represents a version value with metadata about how it was detected.
|
|
8
|
+
# Supports multiple detection methods (command, man_page, etc.) as a
|
|
9
|
+
# fallback hierarchy, NOT mutually exclusive types.
|
|
10
|
+
#
|
|
11
|
+
# @example Command-based version
|
|
12
|
+
# VersionInfo.new(
|
|
13
|
+
# value: '3.11',
|
|
14
|
+
# method_used: :command,
|
|
15
|
+
# available_methods: [:command]
|
|
16
|
+
# )
|
|
17
|
+
#
|
|
18
|
+
# @example Man page fallback version
|
|
19
|
+
# VersionInfo.new(
|
|
20
|
+
# value: '2020-09-21',
|
|
21
|
+
# method_used: :man_page,
|
|
22
|
+
# available_methods: [:command, :man_page]
|
|
23
|
+
# )
|
|
24
|
+
class VersionInfo
|
|
25
|
+
attr_reader :value, :method_used, :available_methods
|
|
26
|
+
|
|
27
|
+
# Initialize version info
|
|
28
|
+
#
|
|
29
|
+
# @param value [String] the version value (e.g., "3.11" or "2020-09-21")
|
|
30
|
+
# @param method_used [Symbol] the method that succeeded (:command, :man_page, etc.)
|
|
31
|
+
# @param available_methods [Array<Symbol>] all methods that were available
|
|
32
|
+
def initialize(value:, method_used:, available_methods: [])
|
|
33
|
+
@value = value
|
|
34
|
+
@method_used = method_used
|
|
35
|
+
@available_methods = available_methods
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Check if version was detected via command
|
|
39
|
+
#
|
|
40
|
+
# @return [Boolean] true if from command execution
|
|
41
|
+
def from_command?
|
|
42
|
+
method_used == :command
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Check if version was detected via man page
|
|
46
|
+
#
|
|
47
|
+
# @return [Boolean] true if from man page date
|
|
48
|
+
def from_man_page?
|
|
49
|
+
method_used == :man_page
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Display format with context
|
|
53
|
+
# Adds "(man page)" suffix only for display, not stored in data
|
|
54
|
+
#
|
|
55
|
+
# @return [String] formatted version string
|
|
56
|
+
def to_s
|
|
57
|
+
case method_used
|
|
58
|
+
when :command
|
|
59
|
+
value
|
|
60
|
+
when :man_page
|
|
61
|
+
"#{value} (man page)"
|
|
62
|
+
else
|
|
63
|
+
value
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Hash representation
|
|
68
|
+
#
|
|
69
|
+
# @return [Hash] hash with value, method, and available_methods
|
|
70
|
+
def to_h
|
|
71
|
+
{
|
|
72
|
+
value: value,
|
|
73
|
+
method: method_used,
|
|
74
|
+
available_methods: available_methods
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Equality comparison
|
|
79
|
+
#
|
|
80
|
+
# @param other [Object] the object to compare
|
|
81
|
+
# @return [Boolean] true if equal
|
|
82
|
+
def ==(other)
|
|
83
|
+
return false unless other.is_a?(VersionInfo)
|
|
84
|
+
|
|
85
|
+
value == other.value &&
|
|
86
|
+
method_used == other.method_used &&
|
|
87
|
+
available_methods == other.available_methods
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Inspect representation
|
|
91
|
+
#
|
|
92
|
+
# @return [String] inspect string
|
|
93
|
+
def inspect
|
|
94
|
+
"#<VersionInfo value=\"#{value}\" method=#{method_used}>"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
data/lib/ukiryu/register.rb
CHANGED
|
@@ -98,7 +98,7 @@ module Ukiryu
|
|
|
98
98
|
# First try exact name match
|
|
99
99
|
yaml_content = load_tool_yaml(name, options.merge(register_path: register_path))
|
|
100
100
|
if yaml_content
|
|
101
|
-
hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol])
|
|
101
|
+
hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true)
|
|
102
102
|
return ToolMetadata.from_hash(hash, tool_name: name.to_s, register_path: register_path) if hash
|
|
103
103
|
end
|
|
104
104
|
|
|
@@ -162,7 +162,7 @@ module Ukiryu
|
|
|
162
162
|
yaml_content = load_tool_yaml(name, options)
|
|
163
163
|
return Models::ValidationResult.not_found(name.to_s) unless yaml_content
|
|
164
164
|
|
|
165
|
-
profile = YAML.safe_load(yaml_content, permitted_classes: [Symbol])
|
|
165
|
+
profile = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true)
|
|
166
166
|
return Models::ValidationResult.invalid(name.to_s, ['Failed to parse YAML']) unless profile
|
|
167
167
|
|
|
168
168
|
errors = SchemaValidator.validate_profile(profile, options)
|
data/lib/ukiryu/thor_ext.rb
CHANGED
|
@@ -183,7 +183,7 @@ module Ukiryu
|
|
|
183
183
|
# @param error [Exception] the error to format
|
|
184
184
|
# @return [String] the formatted error message
|
|
185
185
|
def format_error_message(error)
|
|
186
|
-
message = error.message.to_s
|
|
186
|
+
message = error.message.to_s.dup
|
|
187
187
|
return message if error.is_a?(Thor::Error) && !message.empty?
|
|
188
188
|
|
|
189
189
|
# Add class prefix for non-Thor errors or empty messages
|
data/lib/ukiryu/tool.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative 'version_detector'
|
|
|
13
13
|
require_relative 'logger'
|
|
14
14
|
require_relative 'tool_index'
|
|
15
15
|
require_relative 'models/routing'
|
|
16
|
+
require_relative 'errors'
|
|
16
17
|
|
|
17
18
|
module Ukiryu
|
|
18
19
|
# Tool wrapper class for external command-line tools
|
|
@@ -441,7 +442,17 @@ module Ukiryu
|
|
|
441
442
|
#
|
|
442
443
|
# @return [String, nil] the tool version
|
|
443
444
|
def version
|
|
444
|
-
@version
|
|
445
|
+
return @version if @version
|
|
446
|
+
|
|
447
|
+
info = detect_version
|
|
448
|
+
info&.to_s
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# Get the tool version info (full metadata)
|
|
452
|
+
#
|
|
453
|
+
# @return [Models::VersionInfo, nil] the version info or nil
|
|
454
|
+
def version_info
|
|
455
|
+
@version_info ||= detect_version
|
|
445
456
|
end
|
|
446
457
|
|
|
447
458
|
# Get the definition source if loaded from non-register source
|
|
@@ -719,14 +730,23 @@ module Ukiryu
|
|
|
719
730
|
|
|
720
731
|
# Detect tool version using VersionDetector
|
|
721
732
|
#
|
|
722
|
-
#
|
|
733
|
+
# Supports both legacy format (command/pattern) and new methods array.
|
|
734
|
+
# The methods array allows fallback hierarchy: try command first,
|
|
735
|
+
# then man page, etc.
|
|
736
|
+
#
|
|
737
|
+
# @return [Models::VersionInfo, nil] the version info or nil if not detected
|
|
723
738
|
public
|
|
724
739
|
|
|
725
740
|
def detect_version
|
|
726
741
|
vd = @profile.version_detection
|
|
727
742
|
return nil unless vd
|
|
728
743
|
|
|
729
|
-
#
|
|
744
|
+
# Check for new detection_methods array format
|
|
745
|
+
if vd.respond_to?(:detection_methods) && vd.detection_methods && !vd.detection_methods.empty?
|
|
746
|
+
return detect_version_with_detection_methods(vd.detection_methods)
|
|
747
|
+
end
|
|
748
|
+
|
|
749
|
+
# Legacy format: command-based detection
|
|
730
750
|
return nil if vd.command.nil? || vd.command.empty?
|
|
731
751
|
|
|
732
752
|
# For man page detection, the executable is 'man' and command is the tool name
|
|
@@ -743,7 +763,7 @@ module Ukiryu
|
|
|
743
763
|
command_args = vd.command
|
|
744
764
|
end
|
|
745
765
|
|
|
746
|
-
VersionDetector.
|
|
766
|
+
VersionDetector.detect_info(
|
|
747
767
|
executable: executable,
|
|
748
768
|
command: command_args,
|
|
749
769
|
pattern: vd.pattern || /(\d+\.\d+)/,
|
|
@@ -752,6 +772,61 @@ module Ukiryu
|
|
|
752
772
|
)
|
|
753
773
|
end
|
|
754
774
|
|
|
775
|
+
# Detect version using detection_methods array with fallback hierarchy
|
|
776
|
+
#
|
|
777
|
+
# @param detection_methods [Array] array of method definitions from YAML
|
|
778
|
+
# @return [Models::VersionInfo, nil] version info or nil
|
|
779
|
+
def detect_version_with_detection_methods(detection_methods)
|
|
780
|
+
# Convert YAML detection_methods to format expected by VersionDetector
|
|
781
|
+
detector_methods = detection_methods.map do |m|
|
|
782
|
+
# Handle both Hash and Lutaml::Model objects
|
|
783
|
+
type = if m.respond_to?(:type)
|
|
784
|
+
m.type
|
|
785
|
+
elsif m.is_a?(Hash)
|
|
786
|
+
m[:type] || m['type']
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
if type == :man_page || type == 'man_page'
|
|
790
|
+
paths = if m.respond_to?(:paths)
|
|
791
|
+
m.paths
|
|
792
|
+
elsif m.is_a?(Hash)
|
|
793
|
+
m[:paths] || m['paths']
|
|
794
|
+
else
|
|
795
|
+
{}
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
{
|
|
799
|
+
type: :man_page,
|
|
800
|
+
paths: paths
|
|
801
|
+
}
|
|
802
|
+
else
|
|
803
|
+
command = if m.respond_to?(:command)
|
|
804
|
+
m.command
|
|
805
|
+
elsif m.is_a?(Hash)
|
|
806
|
+
m[:command] || m['command']
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
pattern = if m.respond_to?(:pattern)
|
|
810
|
+
m.pattern
|
|
811
|
+
elsif m.is_a?(Hash)
|
|
812
|
+
m[:pattern] || m['pattern']
|
|
813
|
+
end
|
|
814
|
+
|
|
815
|
+
{
|
|
816
|
+
type: :command,
|
|
817
|
+
command: command || '--version',
|
|
818
|
+
pattern: pattern || /(\d+\.\d+)/
|
|
819
|
+
}
|
|
820
|
+
end
|
|
821
|
+
end
|
|
822
|
+
|
|
823
|
+
VersionDetector.detect_with_methods(
|
|
824
|
+
executable: @executable,
|
|
825
|
+
methods: detector_methods,
|
|
826
|
+
shell: @shell
|
|
827
|
+
)
|
|
828
|
+
end
|
|
829
|
+
|
|
755
830
|
# Check version compatibility with profile requirements
|
|
756
831
|
#
|
|
757
832
|
# @param mode [Symbol] check mode (:strict, :lenient, :probe)
|
|
@@ -773,7 +848,7 @@ module Ukiryu
|
|
|
773
848
|
end
|
|
774
849
|
|
|
775
850
|
# If installed version unknown, probe for it
|
|
776
|
-
installed = detect_version if !installed && mode == :probe
|
|
851
|
+
installed = detect_version&.to_s if !installed && mode == :probe
|
|
777
852
|
|
|
778
853
|
# If still unknown, handle based on mode
|
|
779
854
|
unless installed
|
data/lib/ukiryu/tool_index.rb
CHANGED
|
@@ -160,7 +160,7 @@ module Ukiryu
|
|
|
160
160
|
# Scan all tool directories for metadata
|
|
161
161
|
Dir.glob(File.join(tools_dir, '*', '*.yaml')).each do |file|
|
|
162
162
|
# Load only the top-level keys (metadata) without full parsing
|
|
163
|
-
hash = YAML.safe_load(File.read(file), permitted_classes: [Symbol])
|
|
163
|
+
hash = YAML.safe_load(File.read(file), permitted_classes: [Symbol], aliases: true)
|
|
164
164
|
next unless hash
|
|
165
165
|
|
|
166
166
|
tool_name = File.basename(File.dirname(file))
|
|
@@ -197,7 +197,7 @@ module Ukiryu
|
|
|
197
197
|
yaml_content = load_yaml_for_tool(tool_name)
|
|
198
198
|
return nil unless yaml_content
|
|
199
199
|
|
|
200
|
-
hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol])
|
|
200
|
+
hash = YAML.safe_load(yaml_content, permitted_classes: [Symbol], aliases: true)
|
|
201
201
|
return nil unless hash
|
|
202
202
|
|
|
203
203
|
ToolMetadata.from_hash(hash, tool_name: tool_name.to_s, register_path: register_path)
|
data/lib/ukiryu/version.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'executor'
|
|
4
|
+
require_relative 'models/version_info'
|
|
5
|
+
require_relative 'man_page_parser'
|
|
4
6
|
|
|
5
7
|
module Ukiryu
|
|
6
8
|
# Version detector for external CLI tools
|
|
@@ -10,25 +12,28 @@ module Ukiryu
|
|
|
10
12
|
# - Regex pattern matching for version strings
|
|
11
13
|
# - Proper shell handling for command execution
|
|
12
14
|
# - Support for man-page based version detection (BSD/system tools)
|
|
15
|
+
# - Fallback hierarchy: try multiple methods, use first success
|
|
13
16
|
#
|
|
14
17
|
# @example Detecting version from command output (GNU tools)
|
|
15
|
-
#
|
|
18
|
+
# info = VersionDetector.detect(
|
|
16
19
|
# executable: '/usr/bin/ffmpeg',
|
|
17
20
|
# command: '-version',
|
|
18
21
|
# pattern: /version (\d+\.\d+)/,
|
|
19
22
|
# shell: :bash
|
|
20
23
|
# )
|
|
21
24
|
#
|
|
22
|
-
# @example Detecting version
|
|
23
|
-
#
|
|
24
|
-
# executable: '/usr/bin/
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
25
|
+
# @example Detecting version with fallback hierarchy
|
|
26
|
+
# info = VersionDetector.detect_with_methods(
|
|
27
|
+
# executable: '/usr/bin/xargs',
|
|
28
|
+
# methods: [
|
|
29
|
+
# { type: :command, command: '--version', pattern: /xargs \(GNU findutils\) ([\d.]+)/ },
|
|
30
|
+
# { type: :man_page, paths: { macos: '/usr/share/man/man1/xargs.1' } }
|
|
31
|
+
# ],
|
|
32
|
+
# shell: :bash
|
|
28
33
|
# )
|
|
29
34
|
module VersionDetector
|
|
30
35
|
class << self
|
|
31
|
-
# Detect the version of an external tool
|
|
36
|
+
# Detect the version of an external tool (legacy API)
|
|
32
37
|
#
|
|
33
38
|
# @param executable [String] the executable path
|
|
34
39
|
# @param command [String, Array<String>] the version command (default: '--version')
|
|
@@ -37,6 +42,26 @@ module Ukiryu
|
|
|
37
42
|
# @param source [String] the version source: 'command' (default) or 'man'
|
|
38
43
|
# @return [String, nil] the detected version or nil if not found
|
|
39
44
|
def detect(executable:, command: '--version', pattern: /(\d+\.\d+)/, shell: nil, source: 'command')
|
|
45
|
+
result = detect_info(
|
|
46
|
+
executable: executable,
|
|
47
|
+
command: command,
|
|
48
|
+
pattern: pattern,
|
|
49
|
+
shell: shell,
|
|
50
|
+
source: source
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
result&.value
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Detect version with full info (VersionInfo)
|
|
57
|
+
#
|
|
58
|
+
# @param executable [String] the executable path
|
|
59
|
+
# @param command [String, Array<String>] the version command (default: '--version')
|
|
60
|
+
# @param pattern [Regexp] the regex pattern to extract version
|
|
61
|
+
# @param shell [Symbol] the shell to use for execution
|
|
62
|
+
# @param source [String] the version source: 'command' (default) or 'man'
|
|
63
|
+
# @return [VersionInfo, nil] the version info or nil if not found
|
|
64
|
+
def detect_info(executable:, command: '--version', pattern: /(\d+\.\d+)/, shell: nil, source: 'command')
|
|
40
65
|
# Return nil if executable is not found
|
|
41
66
|
return nil if executable.nil? || executable.empty?
|
|
42
67
|
|
|
@@ -45,7 +70,7 @@ module Ukiryu
|
|
|
45
70
|
# Normalize command to array
|
|
46
71
|
command_args = command.is_a?(Array) ? command : [command]
|
|
47
72
|
|
|
48
|
-
result = Executor.execute(executable, command_args, shell: shell)
|
|
73
|
+
result = Executor.execute(executable, command_args, shell: shell, allow_failure: true)
|
|
49
74
|
|
|
50
75
|
return nil unless result.success?
|
|
51
76
|
|
|
@@ -59,11 +84,83 @@ module Ukiryu
|
|
|
59
84
|
# Get last 500 characters to catch the OS version at bottom
|
|
60
85
|
tail = output[-500..] || output
|
|
61
86
|
match = tail.match(pattern)
|
|
62
|
-
return
|
|
87
|
+
return Models::VersionInfo.new(
|
|
88
|
+
value: match[1],
|
|
89
|
+
method_used: :man_page,
|
|
90
|
+
available_methods: [:man_page]
|
|
91
|
+
) if match
|
|
63
92
|
end
|
|
64
93
|
|
|
65
94
|
match = stdout.match(pattern) || stderr.match(pattern)
|
|
66
|
-
|
|
95
|
+
|
|
96
|
+
return nil unless match
|
|
97
|
+
|
|
98
|
+
Models::VersionInfo.new(
|
|
99
|
+
value: match[1],
|
|
100
|
+
method_used: :command,
|
|
101
|
+
available_methods: [:command]
|
|
102
|
+
)
|
|
103
|
+
rescue StandardError
|
|
104
|
+
# Return nil on any error (command not found, execution error, etc.)
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Detect version using multiple methods with fallback hierarchy
|
|
109
|
+
#
|
|
110
|
+
# Tries each method in order and returns the first successful result.
|
|
111
|
+
# Methods are NOT mutually exclusive - they work together as fallbacks.
|
|
112
|
+
#
|
|
113
|
+
# @param executable [String] the tool executable path
|
|
114
|
+
# @param methods [Array<Hash>] array of method definitions
|
|
115
|
+
# @param shell [Symbol] the shell to use
|
|
116
|
+
# @return [VersionInfo, nil] version info or nil if all methods fail
|
|
117
|
+
def detect_with_methods(executable:, methods:, shell: nil)
|
|
118
|
+
shell ||= Shell.detect
|
|
119
|
+
|
|
120
|
+
# Track available methods for VersionInfo
|
|
121
|
+
available_methods = methods.map { |m| m[:type] }.uniq
|
|
122
|
+
|
|
123
|
+
# Try each method in order
|
|
124
|
+
methods.each do |method|
|
|
125
|
+
case method[:type]
|
|
126
|
+
when :command
|
|
127
|
+
# Try command-based detection
|
|
128
|
+
info = detect_info(
|
|
129
|
+
executable: executable,
|
|
130
|
+
command: method[:command] || '--version',
|
|
131
|
+
pattern: method[:pattern] || /(\d+\.\d+)/,
|
|
132
|
+
shell: shell,
|
|
133
|
+
source: 'command'
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
return info if info
|
|
137
|
+
|
|
138
|
+
when :man_page
|
|
139
|
+
# Try man page date extraction
|
|
140
|
+
paths = method[:paths] || {}
|
|
141
|
+
|
|
142
|
+
# Resolve man page path for current platform
|
|
143
|
+
require_relative 'platform'
|
|
144
|
+
platform = Platform.detect
|
|
145
|
+
man_path = paths[platform] || paths[platform.to_s]
|
|
146
|
+
|
|
147
|
+
next unless man_path
|
|
148
|
+
|
|
149
|
+
# Parse date from man page
|
|
150
|
+
date_str = ManPageParser.parse_date(man_path)
|
|
151
|
+
|
|
152
|
+
next unless date_str
|
|
153
|
+
|
|
154
|
+
return Models::VersionInfo.new(
|
|
155
|
+
value: date_str,
|
|
156
|
+
method_used: :man_page,
|
|
157
|
+
available_methods: available_methods
|
|
158
|
+
)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# All methods failed
|
|
163
|
+
nil
|
|
67
164
|
end
|
|
68
165
|
end
|
|
69
166
|
end
|
data/ukiryu.gemspec
CHANGED
|
@@ -29,7 +29,7 @@ Gem::Specification.new do |spec|
|
|
|
29
29
|
spec.required_ruby_version = '>= 2.7.0'
|
|
30
30
|
|
|
31
31
|
# Core dependencies
|
|
32
|
-
spec.add_dependency 'git', '~>
|
|
32
|
+
spec.add_dependency 'git', '~> 3.0'
|
|
33
33
|
spec.add_dependency 'lutaml-model', '~> 0.7'
|
|
34
34
|
spec.add_dependency 'thor', '~> 1.0'
|
|
35
35
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ukiryu
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '3.0'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
25
|
+
version: '3.0'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: lutaml-model
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -178,6 +178,7 @@ files:
|
|
|
178
178
|
- lib/ukiryu/extractors/native_extractor.rb
|
|
179
179
|
- lib/ukiryu/io.rb
|
|
180
180
|
- lib/ukiryu/logger.rb
|
|
181
|
+
- lib/ukiryu/man_page_parser.rb
|
|
181
182
|
- lib/ukiryu/models.rb
|
|
182
183
|
- lib/ukiryu/models/argument.rb
|
|
183
184
|
- lib/ukiryu/models/argument_definition.rb
|
|
@@ -202,6 +203,7 @@ files:
|
|
|
202
203
|
- lib/ukiryu/models/validation_result.rb
|
|
203
204
|
- lib/ukiryu/models/version_compatibility.rb
|
|
204
205
|
- lib/ukiryu/models/version_detection.rb
|
|
206
|
+
- lib/ukiryu/models/version_info.rb
|
|
205
207
|
- lib/ukiryu/options/base.rb
|
|
206
208
|
- lib/ukiryu/options_builder.rb
|
|
207
209
|
- lib/ukiryu/options_builder/formatter.rb
|