ukiryu 0.1.1 → 0.1.3

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/release.yml +58 -14
  3. data/.gitignore +3 -0
  4. data/.rubocop_todo.yml +170 -79
  5. data/Gemfile +1 -1
  6. data/README.adoc +1603 -576
  7. data/docs/.gitignore +1 -0
  8. data/docs/Gemfile +10 -0
  9. data/docs/INDEX.adoc +261 -0
  10. data/docs/_config.yml +180 -0
  11. data/docs/advanced/custom-tool-classes.adoc +581 -0
  12. data/docs/advanced/index.adoc +20 -0
  13. data/docs/features/configuration.adoc +657 -0
  14. data/docs/features/index.adoc +31 -0
  15. data/docs/features/platform-support.adoc +488 -0
  16. data/docs/getting-started/core-concepts.adoc +666 -0
  17. data/docs/getting-started/index.adoc +36 -0
  18. data/docs/getting-started/installation.adoc +216 -0
  19. data/docs/getting-started/quick-start.adoc +258 -0
  20. data/docs/guides/env-var-sets.adoc +388 -0
  21. data/docs/guides/index.adoc +20 -0
  22. data/docs/interfaces/cli.adoc +609 -0
  23. data/docs/interfaces/index.adoc +153 -0
  24. data/docs/interfaces/ruby-api.adoc +538 -0
  25. data/docs/lychee.toml +49 -0
  26. data/docs/reference/configuration-options.adoc +720 -0
  27. data/docs/reference/error-codes.adoc +634 -0
  28. data/docs/reference/index.adoc +20 -0
  29. data/docs/reference/ruby-api.adoc +1217 -0
  30. data/docs/understanding/index.adoc +20 -0
  31. data/lib/ukiryu/cli.rb +43 -58
  32. data/lib/ukiryu/cli_commands/base_command.rb +16 -27
  33. data/lib/ukiryu/cli_commands/cache_command.rb +100 -0
  34. data/lib/ukiryu/cli_commands/commands_command.rb +8 -8
  35. data/lib/ukiryu/cli_commands/commands_command.rb.fixed +1 -1
  36. data/lib/ukiryu/cli_commands/config_command.rb +49 -7
  37. data/lib/ukiryu/cli_commands/definitions_command.rb +254 -0
  38. data/lib/ukiryu/cli_commands/describe_command.rb +13 -7
  39. data/lib/ukiryu/cli_commands/describe_command.rb.fixed +1 -1
  40. data/lib/ukiryu/cli_commands/docs_command.rb +148 -0
  41. data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +1 -1
  42. data/lib/ukiryu/cli_commands/extract_command.rb +2 -2
  43. data/lib/ukiryu/cli_commands/info_command.rb +7 -7
  44. data/lib/ukiryu/cli_commands/lint_command.rb +167 -0
  45. data/lib/ukiryu/cli_commands/list_command.rb +6 -6
  46. data/lib/ukiryu/cli_commands/opts_command.rb +2 -2
  47. data/lib/ukiryu/cli_commands/opts_command.rb.fixed +1 -1
  48. data/lib/ukiryu/cli_commands/register_command.rb +144 -0
  49. data/lib/ukiryu/cli_commands/resolve_command.rb +124 -0
  50. data/lib/ukiryu/cli_commands/run_command.rb +38 -14
  51. data/lib/ukiryu/cli_commands/run_file_command.rb +2 -2
  52. data/lib/ukiryu/cli_commands/system_command.rb +50 -32
  53. data/lib/ukiryu/cli_commands/validate_command.rb +452 -51
  54. data/lib/ukiryu/cli_commands/which_command.rb +5 -5
  55. data/lib/ukiryu/command_builder.rb +81 -23
  56. data/lib/ukiryu/config/env_provider.rb +3 -3
  57. data/lib/ukiryu/config/env_schema.rb +6 -6
  58. data/lib/ukiryu/config.rb +11 -11
  59. data/lib/ukiryu/definition/definition_cache.rb +238 -0
  60. data/lib/ukiryu/definition/definition_composer.rb +257 -0
  61. data/lib/ukiryu/definition/definition_linter.rb +460 -0
  62. data/lib/ukiryu/definition/definition_validator.rb +320 -0
  63. data/lib/ukiryu/definition/discovery.rb +239 -0
  64. data/lib/ukiryu/definition/documentation_generator.rb +429 -0
  65. data/lib/ukiryu/definition/lint_issue.rb +168 -0
  66. data/lib/ukiryu/definition/loader.rb +139 -0
  67. data/lib/ukiryu/definition/metadata.rb +159 -0
  68. data/lib/ukiryu/definition/source.rb +87 -0
  69. data/lib/ukiryu/definition/sources/file.rb +138 -0
  70. data/lib/ukiryu/definition/sources/string.rb +88 -0
  71. data/lib/ukiryu/definition/validation_result.rb +158 -0
  72. data/lib/ukiryu/definition/version_resolver.rb +194 -0
  73. data/lib/ukiryu/definition.rb +40 -0
  74. data/lib/ukiryu/errors.rb +6 -0
  75. data/lib/ukiryu/execution_context.rb +11 -11
  76. data/lib/ukiryu/executor.rb +6 -0
  77. data/lib/ukiryu/extractors/extractor.rb +6 -5
  78. data/lib/ukiryu/extractors/help_parser.rb +13 -19
  79. data/lib/ukiryu/logger.rb +3 -1
  80. data/lib/ukiryu/models/command_definition.rb +3 -3
  81. data/lib/ukiryu/models/command_info.rb +1 -1
  82. data/lib/ukiryu/models/components.rb +1 -3
  83. data/lib/ukiryu/models/env_var_definition.rb +11 -3
  84. data/lib/ukiryu/models/flag_definition.rb +15 -0
  85. data/lib/ukiryu/models/option_definition.rb +7 -7
  86. data/lib/ukiryu/models/platform_profile.rb +6 -3
  87. data/lib/ukiryu/models/routing.rb +1 -1
  88. data/lib/ukiryu/models/tool_definition.rb +2 -4
  89. data/lib/ukiryu/models/tool_metadata.rb +6 -6
  90. data/lib/ukiryu/models/validation_result.rb +1 -1
  91. data/lib/ukiryu/models/version_compatibility.rb +6 -3
  92. data/lib/ukiryu/models/version_detection.rb +10 -1
  93. data/lib/ukiryu/{registry.rb → register.rb} +54 -38
  94. data/lib/ukiryu/register_auto_manager.rb +268 -0
  95. data/lib/ukiryu/schema_validator.rb +31 -10
  96. data/lib/ukiryu/shell/base.rb +18 -0
  97. data/lib/ukiryu/shell/bash.rb +19 -1
  98. data/lib/ukiryu/shell/cmd.rb +11 -1
  99. data/lib/ukiryu/shell/powershell.rb +11 -1
  100. data/lib/ukiryu/shell.rb +1 -1
  101. data/lib/ukiryu/tool.rb +107 -95
  102. data/lib/ukiryu/tool_index.rb +22 -22
  103. data/lib/ukiryu/tools/base.rb +12 -25
  104. data/lib/ukiryu/tools/generator.rb +7 -7
  105. data/lib/ukiryu/tools.rb +3 -3
  106. data/lib/ukiryu/type.rb +20 -5
  107. data/lib/ukiryu/version.rb +1 -1
  108. data/lib/ukiryu/version_detector.rb +21 -2
  109. data/lib/ukiryu.rb +6 -3
  110. data/ukiryu-proposal.md +41 -41
  111. data/ukiryu.gemspec +1 -0
  112. metadata +64 -8
  113. data/.gitmodules +0 -3
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'type'
4
4
  require_relative 'shell'
5
+ require_relative 'models/env_var_definition'
5
6
 
6
7
  module Ukiryu
7
8
  # CommandBuilder module provides shared command building functionality.
@@ -23,7 +24,19 @@ module Ukiryu
23
24
  # Add subcommand prefix if present (e.g., for ImageMagick "magick convert")
24
25
  args << command.subcommand if command.subcommand
25
26
 
26
- # Add options first (before arguments)
27
+ # Add prefix flags FIRST (must come before any options)
28
+ command.flags&.each do |flag_def|
29
+ param_key = flag_def.name_sym
30
+ next unless flag_def.position_constraint_sym == :prefix
31
+
32
+ value = params[param_key]
33
+ value = flag_def.default if value.nil?
34
+
35
+ formatted_flag = format_flag(flag_def, value)
36
+ Array(formatted_flag).each { |flag| args << flag unless flag.nil? || flag.empty? }
37
+ end
38
+
39
+ # Add options (after prefix flags)
27
40
  command.options&.each do |opt_def|
28
41
  param_key = opt_def.name_sym
29
42
  next unless params.key?(param_key)
@@ -33,9 +46,11 @@ module Ukiryu
33
46
  Array(formatted_opt).each { |opt| args << opt unless opt.nil? || opt.empty? }
34
47
  end
35
48
 
36
- # Add flags
49
+ # Add non-prefix flags (after options)
37
50
  command.flags&.each do |flag_def|
38
51
  param_key = flag_def.name_sym
52
+ next if flag_def.position_constraint_sym == :prefix
53
+
39
54
  value = params[param_key]
40
55
  value = flag_def.default if value.nil?
41
56
 
@@ -127,43 +142,59 @@ module Ukiryu
127
142
  end
128
143
 
129
144
  cli = opt_def.cli || ''
130
- format_sym = opt_def.format_sym
145
+ delimiter_sym = opt_def.assignment_delimiter_sym
131
146
  separator = opt_def.separator || '='
132
147
 
148
+ # Auto-detect delimiter based on CLI prefix
149
+ delimiter_sym = detect_delimiter(cli) if delimiter_sym == :auto
150
+
133
151
  # Convert value to string (handle symbols)
134
152
  value_str = value.is_a?(Symbol) ? value.to_s : value.to_s
135
153
 
136
154
  # Handle array values with separator
137
155
  if value.is_a?(Array) && separator
138
156
  joined = value.join(separator)
139
- case format_sym
140
- when :double_dash_equals
141
- "#{cli}#{joined}"
142
- when :double_dash_space, :single_dash_space
157
+ case delimiter_sym
158
+ when :equals
159
+ "#{cli}=#{joined}"
160
+ when :space
143
161
  [cli, joined] # Return array for space-separated
144
- when :single_dash_equals
145
- "#{cli}#{joined}"
162
+ when :colon
163
+ "#{cli}:#{joined}"
164
+ when :none
165
+ cli
146
166
  else
147
- "#{cli}#{joined}"
167
+ "#{cli}=#{joined}"
148
168
  end
149
169
  else
150
- case format_sym
151
- when :double_dash_equals
152
- "#{cli}#{separator}#{value_str}"
153
- when :double_dash_space, :single_dash_space
170
+ case delimiter_sym
171
+ when :equals
172
+ "#{cli}=#{value_str}"
173
+ when :space
154
174
  [cli, value_str] # Return array for space-separated
155
- when :single_dash_equals
156
- "#{cli}#{separator}#{value_str}"
157
- when :slash_colon
175
+ when :colon
158
176
  "#{cli}:#{value_str}"
159
- when :slash_space
160
- "#{cli} #{value_str}"
177
+ when :none
178
+ cli
161
179
  else
162
- "#{cli}#{separator}#{value_str}"
180
+ "#{cli}=#{value_str}"
163
181
  end
164
182
  end
165
183
  end
166
184
 
185
+ # Detect assignment delimiter based on CLI prefix
186
+ #
187
+ # @param cli [String] the CLI flag
188
+ # @return [Symbol] the delimiter
189
+ def detect_delimiter(cli)
190
+ case cli
191
+ when /^--/ then :equals # --flag=value
192
+ when /^-/ then :space # -f value
193
+ when %r{^/} then :colon # /format:value
194
+ else :equals
195
+ end
196
+ end
197
+
167
198
  # Format a flag
168
199
  #
169
200
  # @param flag_def [Models::FlagDefinition] the flag definition
@@ -178,11 +209,38 @@ module Ukiryu
178
209
  # Build environment variables for command
179
210
  #
180
211
  # @param command [Models::CommandDefinition] the command definition
212
+ # @param profile [Models::PlatformProfile] the profile (for env_var_sets)
181
213
  # @param params [Hash] the parameters hash
182
214
  # @return [Hash] the environment variables hash
183
- def build_env_vars(command, params)
215
+ def build_env_vars(command, profile, params)
184
216
  env_vars = {}
185
217
 
218
+ # First, add env vars from sets specified in use_env_vars
219
+ command.use_env_vars&.each do |set_name|
220
+ set = profile.env_var_sets&.dig(set_name.to_s)
221
+ next unless set
222
+
223
+ set.each do |ev_data|
224
+ # Convert hash to EnvVarDefinition if needed
225
+ ev = ev_data.is_a?(Hash) ? Models::EnvVarDefinition.new(ev_data) : ev_data
226
+
227
+ # Check platform restriction
228
+ platforms = ev.platforms || []
229
+ next if platforms.any? && !platforms.map(&:to_sym).include?(@platform)
230
+
231
+ # Get value - use ev.value if provided, or extract from params
232
+ value = if ev.value
233
+ ev.value
234
+ elsif ev.from
235
+ params[ev.from.to_sym]
236
+ end
237
+
238
+ # Set the environment variable if value is defined (including empty string)
239
+ env_vars[ev.name] = value.to_s unless value.nil?
240
+ end
241
+ end
242
+
243
+ # Then, add command's own env_vars (can override set values)
186
244
  command.env_vars&.each do |ev|
187
245
  # Check platform restriction
188
246
  platforms = ev.platforms || []
@@ -191,8 +249,8 @@ module Ukiryu
191
249
  # Get value - use ev.value if provided, or extract from params
192
250
  value = if ev.value
193
251
  ev.value
194
- elsif ev.env_var
195
- params[ev.env_var.to_sym]
252
+ elsif ev.from
253
+ params[ev.from.to_sym]
196
254
  end
197
255
 
198
256
  # Set the environment variable if value is defined (including empty string)
@@ -40,9 +40,9 @@ module Ukiryu
40
40
  load_attributes(EnvSchema.all_output_attributes)
41
41
  end
42
42
 
43
- # Load registry-specific environment overrides
44
- def load_registry
45
- load_attributes(EnvSchema.all_registry_attributes)
43
+ # Load register-specific environment overrides
44
+ def load_register
45
+ load_attributes(EnvSchema.all_register_attributes)
46
46
  end
47
47
 
48
48
  private
@@ -17,8 +17,8 @@ module Ukiryu
17
17
  format: :symbol,
18
18
  output: :string,
19
19
 
20
- # Registry options
21
- registry: :string,
20
+ # Register options
21
+ register: :string,
22
22
 
23
23
  # Tool discovery options
24
24
  search_paths: :string, # Comma-separated paths
@@ -48,14 +48,14 @@ module Ukiryu
48
48
  %i[format output use_color]
49
49
  end
50
50
 
51
- # All registry attributes
52
- def all_registry_attributes
53
- %i[registry search_paths]
51
+ # All register attributes
52
+ def all_register_attributes
53
+ %i[register search_paths]
54
54
  end
55
55
 
56
56
  # All attributes
57
57
  def all_attributes
58
- %i[timeout debug dry_run metrics shell format output registry search_paths use_color]
58
+ %i[timeout debug dry_run metrics shell format output register search_paths use_color]
59
59
  end
60
60
  end
61
61
  end
data/lib/ukiryu/config.rb CHANGED
@@ -129,16 +129,16 @@ module Ukiryu
129
129
  @resolver.set_programmatic(:output, value)
130
130
  end
131
131
 
132
- # Registry path
133
- # @return [String, nil] path to tool registry
134
- def registry
135
- @resolver.resolve(:registry)
132
+ # Register path
133
+ # @return [String, nil] path to tool register
134
+ def register
135
+ @resolver.resolve(:register)
136
136
  end
137
137
 
138
- # Set registry path
139
- # @param value [String] path to tool registry
140
- def registry=(value)
141
- @resolver.set_programmatic(:registry, value)
138
+ # Set register path
139
+ # @param value [String] path to tool register
140
+ def register=(value)
141
+ @resolver.set_programmatic(:register, value)
142
142
  end
143
143
 
144
144
  # Tool search paths (comma-separated)
@@ -214,7 +214,7 @@ module Ukiryu
214
214
  shell: shell,
215
215
  format: format,
216
216
  output: output,
217
- registry: registry,
217
+ register: register,
218
218
  search_paths: search_paths,
219
219
  use_color: use_color
220
220
  }
@@ -231,9 +231,9 @@ module Ukiryu
231
231
  shell: nil,
232
232
  format: :yaml,
233
233
  output: nil,
234
- registry: nil,
234
+ register: nil,
235
235
  search_paths: nil,
236
- use_color: nil # nil means auto-detect
236
+ use_color: nil
237
237
  }
238
238
 
239
239
  env = EnvProvider.load_all
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require_relative 'metadata'
5
+
6
+ module Ukiryu
7
+ module Definition
8
+ # Cache for tool definitions with hot-reload support
9
+ #
10
+ # This class provides caching for tool definitions with automatic
11
+ # invalidation based on file modification time and TTL.
12
+ class DefinitionCache
13
+ # Default cache TTL in seconds (5 minutes)
14
+ DEFAULT_TTL = 300
15
+
16
+ # Cache entry structure
17
+ class Entry
18
+ attr_reader :definition, :mtime, :loaded_at, :source_key
19
+
20
+ def initialize(definition, mtime: nil, source_key: nil)
21
+ @definition = definition
22
+ @mtime = mtime
23
+ @loaded_at = Time.now
24
+ @source_key = source_key || generate_source_key(definition)
25
+ end
26
+
27
+ # Check if entry is stale
28
+ #
29
+ # @param ttl [Integer] time to live in seconds
30
+ # @return [Boolean] true if entry is stale
31
+ def stale?(ttl: DEFAULT_TTL)
32
+ # Check TTL
33
+ return true if Time.now - @loaded_at > ttl
34
+
35
+ # Check mtime for file-based definitions
36
+ if @mtime && @definition.respond_to?(:path)
37
+ path = @definition.path
38
+ return true if path && File.exist?(path) && File.mtime(path) > @mtime
39
+ end
40
+
41
+ false
42
+ end
43
+
44
+ # Refresh the entry
45
+ #
46
+ # @return [Entry] refreshed entry
47
+ def refresh
48
+ if @definition.respond_to?(:load_definition)
49
+ # Reload from metadata
50
+ new_def = @definition.load_definition
51
+ Entry.new(new_def, mtime: new_def.mtime, source_key: @source_key)
52
+ else
53
+ # Can't refresh, return as-is
54
+ self
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def generate_source_key(definition)
61
+ # Generate cache key from definition
62
+ if definition.respond_to?(:name) && definition.respond_to?(:version)
63
+ Digest::SHA256.hexdigest("#{definition.name}/#{definition.version}")
64
+ else
65
+ Digest::SHA256.hexdigest(definition.object_id.to_s)
66
+ end
67
+ end
68
+ end
69
+
70
+ # Singleton instance
71
+ @instance = nil
72
+
73
+ class << self
74
+ # Get the singleton instance
75
+ #
76
+ # @return [DefinitionCache] the cache instance
77
+ def instance
78
+ @instance ||= new
79
+ end
80
+
81
+ # Reset the singleton (useful for testing)
82
+ def reset_instance
83
+ @instance = nil
84
+ end
85
+ end
86
+
87
+ # Initialize a new cache
88
+ #
89
+ # @param ttl [Integer] default time-to-live in seconds
90
+ def initialize(ttl: DEFAULT_TTL)
91
+ @cache = {}
92
+ @ttl = ttl
93
+ @refresh_strategy = :lazy
94
+ @mutex = Mutex.new
95
+ end
96
+
97
+ # Get a cached definition
98
+ #
99
+ # @param key [String] the cache key
100
+ # @return [Models::ToolDefinition, nil] the cached definition, or nil
101
+ def get(key)
102
+ @mutex.synchronize do
103
+ entry = @cache[key]
104
+ return nil unless entry
105
+
106
+ # Check staleness
107
+ if entry.stale?(ttl: @ttl)
108
+ if @refresh_strategy == :lazy
109
+ # Refresh and return
110
+ entry = entry.refresh
111
+ @cache[key] = entry if entry
112
+ else
113
+ # Return stale entry (eager mode refreshes in background)
114
+ end
115
+ end
116
+ entry.definition
117
+ end
118
+ end
119
+
120
+ # Set a cached definition
121
+ #
122
+ # @param key [String] the cache key
123
+ # @param definition [Models::ToolDefinition] the definition to cache
124
+ # @param metadata [DefinitionMetadata, nil] optional metadata for mtime tracking
125
+ # @return [Models::ToolDefinition] the cached definition
126
+ def set(key, definition, metadata: nil)
127
+ @mutex.synchronize do
128
+ mtime = metadata&.mtime
129
+ entry = Entry.new(definition, mtime: mtime)
130
+ @cache[key] = entry
131
+ definition
132
+ end
133
+ end
134
+
135
+ # Check if a key exists in cache
136
+ #
137
+ # @param key [String] the cache key
138
+ # @return [Boolean] true if key exists
139
+ def key?(key)
140
+ @cache.key?(key)
141
+ end
142
+
143
+ # Invalidate a cache entry
144
+ #
145
+ # @param key [String] the cache key to invalidate
146
+ # @return [Boolean] true if entry was invalidated
147
+ def invalidate(key)
148
+ @mutex.synchronize do
149
+ !@cache.delete(key).nil?
150
+ end
151
+ end
152
+
153
+ # Clear all cache entries
154
+ #
155
+ # @return [Integer] number of entries cleared
156
+ def clear
157
+ @mutex.synchronize do
158
+ count = @cache.size
159
+ @cache.clear
160
+ count
161
+ end
162
+ end
163
+
164
+ # Get cache statistics
165
+ #
166
+ # @return [Hash] cache statistics
167
+ def stats
168
+ {
169
+ size: @cache.size,
170
+ ttl: @ttl,
171
+ refresh_strategy: @refresh_strategy,
172
+ entries: @cache.keys
173
+ }
174
+ end
175
+
176
+ # Check if a cache entry is stale
177
+ #
178
+ # @param key [String] the cache key
179
+ # @return [Boolean] true if entry is stale
180
+ def stale?(key)
181
+ entry = @cache[key]
182
+ return true unless entry
183
+
184
+ entry.stale?(ttl: @ttl)
185
+ end
186
+
187
+ # Set the refresh strategy
188
+ #
189
+ # @param strategy [Symbol] :lazy or :eager
190
+ def refresh_strategy=(strategy)
191
+ raise ArgumentError, "Invalid refresh strategy: #{strategy}" unless %i[lazy eager never].include?(strategy)
192
+
193
+ @refresh_strategy = strategy
194
+ end
195
+
196
+ # Get the current refresh strategy
197
+ #
198
+ # @return [Symbol] the refresh strategy
199
+ attr_reader :refresh_strategy
200
+
201
+ # Set the cache TTL
202
+ #
203
+ # @param ttl [Integer] time-to-live in seconds
204
+
205
+ # Get the current TTL
206
+ #
207
+ # @return [Integer] the TTL in seconds
208
+ attr_accessor :ttl
209
+
210
+ # Refresh all stale entries
211
+ #
212
+ # @return [Integer] number of entries refreshed
213
+ def refresh_stale
214
+ @mutex.synchronize do
215
+ count = 0
216
+ @cache.each do |key, entry|
217
+ if entry.stale?(ttl: @ttl)
218
+ @cache[key] = entry.refresh
219
+ count += 1
220
+ end
221
+ end
222
+ count
223
+ end
224
+ end
225
+
226
+ # Prune stale entries
227
+ #
228
+ # @return [Integer] number of entries pruned
229
+ def prune
230
+ @mutex.synchronize do
231
+ stale_keys = @cache.select { |_, entry| entry.stale?(ttl: @ttl) }.keys
232
+ stale_keys.each { |key| @cache.delete(key) }
233
+ stale_keys.size
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end