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
@@ -0,0 +1,268 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'git'
4
+ require 'fileutils'
5
+
6
+ module Ukiryu
7
+ # Manages automatic register cloning and updates
8
+ #
9
+ # This class handles:
10
+ # - Auto-cloning the register repository to ~/.ukiryu/register
11
+ # - Detecting development mode (local submodule)
12
+ # - Validating register integrity
13
+ # - Providing register path to the Register class
14
+ #
15
+ # @api private
16
+ class RegisterAutoManager
17
+ # GitHub repository URL for the register
18
+ REGISTER_URL = 'https://github.com/ukiryu/register'
19
+
20
+ # Default local directory for the register
21
+ DEFAULT_DIR = '~/.ukiryu/register'
22
+
23
+ class << self
24
+ # Get the register path, ensuring it exists
25
+ #
26
+ # Checks in order:
27
+ # 1. Environment variable UKIRYU_REGISTER
28
+ # 2. User's local clone (~/.ukiryu/register)
29
+ #
30
+ # @return [String, nil] the register path, or nil if unavailable
31
+ def register_path
32
+ # 1. Environment variable has highest priority
33
+ env_path = ENV['UKIRYU_REGISTER']
34
+ return env_path if env_path && Dir.exist?(env_path)
35
+
36
+ # 2. Use user's local clone, create if needed
37
+ ensure_user_clone
38
+ end
39
+
40
+ # Check if the register exists and is valid
41
+ #
42
+ # @return [Boolean] true if register exists and is valid
43
+ def register_exists?
44
+ path = resolve_register_path
45
+ return false unless path
46
+
47
+ Dir.exist?(path) && validate_register_integrity(path)
48
+ end
49
+
50
+ # Update or re-clone the register
51
+ #
52
+ # @param force [Boolean] if true, re-clone even if register exists
53
+ # @return [Boolean] true if successful
54
+ # @raise [RegisterError] if update fails
55
+ def update_register(force: false)
56
+ if force
57
+ force_reclone
58
+ else
59
+ update_existing_clone
60
+ end
61
+ true
62
+ rescue Git::GitExecuteError => e
63
+ raise RegisterError, "Failed to update register: #{e.message}"
64
+ rescue StandardError => e
65
+ raise RegisterError, "Register update failed: #{e.message}"
66
+ end
67
+
68
+ # Get register information
69
+ #
70
+ # @return [Hash] register information
71
+ def register_info
72
+ path = resolve_register_path
73
+ return { status: :not_found } unless path
74
+
75
+ return { status: :not_cloned, path: expand_path(DEFAULT_DIR) } unless Dir.exist?(path)
76
+
77
+ return { status: :invalid, path: path } unless validate_register_integrity(path)
78
+
79
+ info = {
80
+ status: :ok,
81
+ path: path,
82
+ source: detect_source(path)
83
+ }
84
+
85
+ # Add git info if available
86
+ git_dir = File.join(path, '.git')
87
+ if Dir.exist?(git_dir)
88
+ begin
89
+ g = Git.open(path)
90
+ info[:branch] = g.current_branch
91
+ log = g.log(1).execute
92
+ info[:commit] = log.first.sha[0..7]
93
+ info[:last_update] = Time.at(log.first.date.to_i)
94
+ rescue Git::GitExecuteError
95
+ # Git info not available, but register is valid
96
+ end
97
+ end
98
+
99
+ # Count available tools
100
+ tools_dir = File.join(path, 'tools')
101
+ info[:tools_count] = Dir.glob(File.join(tools_dir, '*')).select { |d| File.directory?(d) }.count if Dir.exist?(tools_dir)
102
+
103
+ info
104
+ end
105
+
106
+ private
107
+
108
+ # Ensure the user's local clone exists
109
+ #
110
+ # @return [String, nil] the register path, or nil if unavailable
111
+ def ensure_user_clone
112
+ expanded_path = expand_path(DEFAULT_DIR)
113
+
114
+ # If already exists and valid, return it
115
+ if Dir.exist?(expanded_path)
116
+ return expanded_path if validate_register_integrity(expanded_path)
117
+
118
+ # Exists but invalid, re-clone
119
+ force_reclone
120
+
121
+ return expanded_path
122
+ end
123
+
124
+ # Doesn't exist, clone it
125
+ clone_register(expanded_path)
126
+ expanded_path
127
+ rescue RegisterError
128
+ # Re-raise with context
129
+ raise
130
+ rescue StandardError => e
131
+ raise RegisterError, "Failed to setup register at #{expanded_path}: #{e.message}"
132
+ end
133
+
134
+ # Clone the register repository
135
+ #
136
+ # @param target_path [String] where to clone
137
+ # @raise [RegisterError] if clone fails
138
+ def clone_register(target_path)
139
+ parent_dir = File.dirname(target_path)
140
+
141
+ # Create parent directory if needed
142
+ FileUtils.mkdir_p(parent_dir) unless Dir.exist?(parent_dir)
143
+
144
+ # Check if git is available
145
+ unless git_available?
146
+ raise RegisterError, <<~ERROR
147
+ Git is required but not found in PATH.
148
+
149
+ To fix this:
150
+ 1. Install git from https://git-scm.com
151
+ 2. Or set UKIRYU_REGISTER to use a local register path
152
+
153
+ Example:
154
+ export UKIRYU_REGISTER=/path/to/register
155
+ ERROR
156
+ end
157
+
158
+ # Perform the clone
159
+ print "Cloning register from #{REGISTER_URL}..." if $stdout.tty?
160
+ Git.clone(REGISTER_URL, target_path, quiet: true)
161
+ puts 'done' if $stdout.tty?
162
+
163
+ # Validate the clone
164
+ unless validate_register_integrity(target_path)
165
+ FileUtils.rm_rf(target_path)
166
+ raise RegisterError, 'Register clone validation failed. Please try again or set UKIRYU_REGISTER.'
167
+ end
168
+ rescue Git::GitExecuteError => e
169
+ raise RegisterError, <<~ERROR
170
+ Failed to clone register from #{REGISTER_URL}: #{e.message}
171
+
172
+ To fix this:
173
+ 1. Check your internet connection
174
+ 2. Manually clone: git clone #{REGISTER_URL} #{target_path}
175
+ 3. Or set UKIRYU_REGISTER to use a local register path
176
+
177
+ Example:
178
+ export UKIRYU_REGISTER=/path/to/register
179
+ ERROR
180
+ end
181
+
182
+ # Update existing register clone
183
+ #
184
+ # @raise [RegisterError] if update fails
185
+ def update_existing_clone
186
+ path = expand_path(DEFAULT_DIR)
187
+
188
+ return clone_register(path) unless Dir.exist?(path)
189
+
190
+ begin
191
+ print 'Updating register...' if $stdout.tty?
192
+ g = Git.open(path)
193
+ g.pull
194
+ puts 'done' if $stdout.tty?
195
+ rescue Git::GitExecuteError => e
196
+ raise RegisterError, "Failed to update register: #{e.message}"
197
+ end
198
+ end
199
+
200
+ # Force re-clone the register
201
+ #
202
+ # @raise [RegisterError] if re-clone fails
203
+ def force_reclone
204
+ path = expand_path(DEFAULT_DIR)
205
+ FileUtils.rm_rf(path) if Dir.exist?(path)
206
+ clone_register(path)
207
+ end
208
+
209
+ # Validate register integrity
210
+ #
211
+ # @param path [String] path to check
212
+ # @return [Boolean] true if valid
213
+ def validate_register_integrity(path)
214
+ return false unless path
215
+
216
+ # Check for tools/ directory
217
+ tools_dir = File.join(path, 'tools')
218
+ return false unless Dir.exist?(tools_dir)
219
+
220
+ # Check for at least one tool definition
221
+ # This confirms it's a valid register structure
222
+ Dir.glob(File.join(tools_dir, '*', '*.yaml')).any?
223
+ end
224
+
225
+ # Resolve the register path without auto-creating
226
+ #
227
+ # @return [String, nil] current register path or nil
228
+ def resolve_register_path
229
+ # Check environment variable
230
+ env_path = ENV['UKIRYU_REGISTER']
231
+ return env_path if env_path && Dir.exist?(env_path)
232
+
233
+ # Check user clone
234
+ expanded = expand_path(DEFAULT_DIR)
235
+ Dir.exist?(expanded) ? expanded : nil
236
+ end
237
+
238
+ # Detect the source of the register
239
+ #
240
+ # @param path [String] register path
241
+ # @return [Symbol] :env or :user
242
+ def detect_source(path)
243
+ env_path = ENV['UKIRYU_REGISTER']
244
+ return :env if env_path && path == File.expand_path(env_path)
245
+
246
+ :user
247
+ end
248
+
249
+ # Check if git is available
250
+ #
251
+ # @return [Boolean] true if git binary is available
252
+ def git_available?
253
+ system('git --version > /dev/null 2>&1')
254
+ end
255
+
256
+ # Expand a path with ~ support
257
+ #
258
+ # @param path [String] path to expand
259
+ # @return [String] expanded path
260
+ def expand_path(path)
261
+ File.expand_path(path)
262
+ end
263
+ end
264
+
265
+ # Register-specific error
266
+ class RegisterError < StandardError; end
267
+ end
268
+ end
@@ -31,10 +31,14 @@ module Ukiryu
31
31
  schema = load_schema(options[:schema_path])
32
32
  return ['Failed to load schema'] unless schema
33
33
 
34
+ # Convert symbol keys to strings for JSON Schema validation
35
+ # JSON Schema validators expect string keys, but YAML.safe_load produces symbol keys
36
+ stringified_profile = stringify_keys(profile)
37
+
34
38
  # Validate against JSON schema
35
39
  begin
36
40
  # JSON Schema library expects the data to be a hash
37
- validation_errors = JSON::Validator.fully_validate(schema, profile, strict: options[:strict] || false)
41
+ validation_errors = JSON::Validator.fully_validate(schema, stringified_profile, strict: options[:strict] || false)
38
42
 
39
43
  # Convert errors to readable format
40
44
  validation_errors.each do |error|
@@ -65,16 +69,10 @@ module Ukiryu
65
69
 
66
70
  # Get the default schema path
67
71
  #
68
- # @return [String, nil] the default schema path
72
+ # @return [String, nil] the default schema path (from UKIRYU_SCHEMA_PATH env var)
69
73
  def default_schema_path
70
- # Schema is in the sibling 'schema' directory at the same level as the gem
71
- # From lib/ukiryu/, we go up to gem root, then to sibling schema/
72
- gem_root = File.expand_path('../..', __dir__) # ukiryu gem root
73
- schema_dir = File.expand_path('../schema', gem_root) # src/ukiryu/schema/
74
- schema_file = File.join(schema_dir, 'tool-profile.schema.yaml')
75
- return schema_file if File.exist?(schema_file)
76
-
77
- nil
74
+ # Check environment variable for schema path
75
+ ENV['UKIRYU_SCHEMA_PATH']
78
76
  end
79
77
 
80
78
  private
@@ -96,6 +94,29 @@ module Ukiryu
96
94
  def format_schema_error(error)
97
95
  error
98
96
  end
97
+
98
+ # Convert symbol keys to strings for JSON Schema validation
99
+ #
100
+ # JSON Schema validators expect string keys in the data structure,
101
+ # but YAML.safe_load produces symbol keys. This method recursively
102
+ # converts all symbol keys to strings while preserving values.
103
+ #
104
+ # @param hash [Hash] the hash with symbol keys
105
+ # @return [Hash] hash with string keys
106
+ def stringify_keys(hash)
107
+ return hash unless hash.is_a?(Hash)
108
+
109
+ hash.transform_keys(&:to_s).transform_values do |v|
110
+ case v
111
+ when Hash
112
+ stringify_keys(v)
113
+ when Array
114
+ v.map { |item| item.is_a?(Hash) ? stringify_keys(item) : item }
115
+ else
116
+ v
117
+ end
118
+ end
119
+ end
99
120
  end
100
121
  end
101
122
  end
@@ -27,6 +27,24 @@ module Ukiryu
27
27
  raise NotImplementedError, "#{self.class} must implement #escape"
28
28
  end
29
29
 
30
+ # Check if a string needs quoting
31
+ # Strings with spaces, special chars, or empty strings need quoting
32
+ #
33
+ # @param string [String] the string to check
34
+ # @return [Boolean] true if quoting is needed
35
+ def needs_quoting?(string)
36
+ str = string.to_s
37
+ # Empty strings need quoting
38
+ return true if str.empty?
39
+ # Strings with whitespace need quoting
40
+ return true if str =~ /\s/
41
+ # Strings with shell special characters need quoting
42
+ # Common special chars: $ & * ( ) [ ] { } | ; < > ? ` ~ ! # @ % "
43
+ return true if str =~ /[\s&*()\[\]{}|;<>?`~!@%"]/
44
+
45
+ false
46
+ end
47
+
30
48
  # Quote an argument for this shell
31
49
  #
32
50
  # @param string [String] the string to quote
@@ -51,9 +51,27 @@ module Ukiryu
51
51
 
52
52
  # Get headless environment (disable DISPLAY on Unix)
53
53
  #
54
+ # For macOS, adds additional variables to prevent GUI initialization
55
+ # that can cause crashes in GUI applications like Inkscape.
56
+ #
54
57
  # @return [Hash] environment variables for headless operation
55
58
  def headless_environment
56
- { 'DISPLAY' => '' }
59
+ require_relative '../platform'
60
+
61
+ env = {}
62
+
63
+ # Completely remove DISPLAY instead of setting to empty string
64
+ # This ensures full headless mode with no display connection
65
+ # The executor will exclude this key from the environment entirely
66
+
67
+ # Add macOS-specific environment variables to prevent GUI initialization
68
+ if Platform.detect == :macos
69
+ env['NSAppleEventsSuppressStartupAlert'] = 'true' # Suppress Apple Events
70
+ env['NSUIElement'] = '1' # Run as background agent
71
+ env['GDK_BACKEND'] = 'x11' # Force X11 backend (respects missing DISPLAY)
72
+ end
73
+
74
+ env
57
75
  end
58
76
  end
59
77
  end
@@ -56,12 +56,22 @@ module Ukiryu
56
56
  end
57
57
 
58
58
  # Join executable and arguments into a command line
59
+ # Uses smart quoting: only quote arguments that need it
59
60
  #
60
61
  # @param executable [String] the executable path
61
62
  # @param args [Array<String>] the arguments
62
63
  # @return [String] the complete command line
63
64
  def join(executable, *args)
64
- [executable, *args.map { |a| quote(a) }].join(' ')
65
+ args_formatted = args.map do |a|
66
+ if needs_quoting?(a)
67
+ quote(a)
68
+ else
69
+ # For simple strings, pass without quotes
70
+ # cmd.exe treats them as literal strings
71
+ escape(a)
72
+ end
73
+ end
74
+ [executable, *args_formatted].join(' ')
65
75
  end
66
76
 
67
77
  # cmd.exe doesn't need DISPLAY variable
@@ -41,12 +41,22 @@ module Ukiryu
41
41
  end
42
42
 
43
43
  # Join executable and arguments into a command line
44
+ # Uses smart quoting: only quote arguments that need it
44
45
  #
45
46
  # @param executable [String] the executable path
46
47
  # @param args [Array<String>] the arguments
47
48
  # @return [String] the complete command line
48
49
  def join(executable, *args)
49
- [executable, *args.map { |a| quote(a) }].join(' ')
50
+ args_formatted = args.map do |a|
51
+ if needs_quoting?(a)
52
+ quote(a)
53
+ else
54
+ # For simple strings, pass without quotes
55
+ # PowerShell treats them as literal strings
56
+ a
57
+ end
58
+ end
59
+ [executable, *args_formatted].join(' ')
50
60
  end
51
61
 
52
62
  # PowerShell doesn't need DISPLAY variable
data/lib/ukiryu/shell.rb CHANGED
@@ -73,7 +73,7 @@ module Ukiryu
73
73
  when :powershell
74
74
  powershell_available?
75
75
  when :cmd
76
- true # cmd is always available on Windows
76
+ Platform.windows? # cmd is only available on Windows
77
77
  else
78
78
  false
79
79
  end