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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +58 -14
- data/.gitignore +3 -0
- data/.rubocop_todo.yml +170 -79
- data/Gemfile +1 -1
- data/README.adoc +1603 -576
- data/docs/.gitignore +1 -0
- data/docs/Gemfile +10 -0
- data/docs/INDEX.adoc +261 -0
- data/docs/_config.yml +180 -0
- data/docs/advanced/custom-tool-classes.adoc +581 -0
- data/docs/advanced/index.adoc +20 -0
- data/docs/features/configuration.adoc +657 -0
- data/docs/features/index.adoc +31 -0
- data/docs/features/platform-support.adoc +488 -0
- data/docs/getting-started/core-concepts.adoc +666 -0
- data/docs/getting-started/index.adoc +36 -0
- data/docs/getting-started/installation.adoc +216 -0
- data/docs/getting-started/quick-start.adoc +258 -0
- data/docs/guides/env-var-sets.adoc +388 -0
- data/docs/guides/index.adoc +20 -0
- data/docs/interfaces/cli.adoc +609 -0
- data/docs/interfaces/index.adoc +153 -0
- data/docs/interfaces/ruby-api.adoc +538 -0
- data/docs/lychee.toml +49 -0
- data/docs/reference/configuration-options.adoc +720 -0
- data/docs/reference/error-codes.adoc +634 -0
- data/docs/reference/index.adoc +20 -0
- data/docs/reference/ruby-api.adoc +1217 -0
- data/docs/understanding/index.adoc +20 -0
- data/lib/ukiryu/cli.rb +43 -58
- data/lib/ukiryu/cli_commands/base_command.rb +16 -27
- data/lib/ukiryu/cli_commands/cache_command.rb +100 -0
- data/lib/ukiryu/cli_commands/commands_command.rb +8 -8
- data/lib/ukiryu/cli_commands/commands_command.rb.fixed +1 -1
- data/lib/ukiryu/cli_commands/config_command.rb +49 -7
- data/lib/ukiryu/cli_commands/definitions_command.rb +254 -0
- data/lib/ukiryu/cli_commands/describe_command.rb +13 -7
- data/lib/ukiryu/cli_commands/describe_command.rb.fixed +1 -1
- data/lib/ukiryu/cli_commands/docs_command.rb +148 -0
- data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +1 -1
- data/lib/ukiryu/cli_commands/extract_command.rb +2 -2
- data/lib/ukiryu/cli_commands/info_command.rb +7 -7
- data/lib/ukiryu/cli_commands/lint_command.rb +167 -0
- data/lib/ukiryu/cli_commands/list_command.rb +6 -6
- data/lib/ukiryu/cli_commands/opts_command.rb +2 -2
- data/lib/ukiryu/cli_commands/opts_command.rb.fixed +1 -1
- data/lib/ukiryu/cli_commands/register_command.rb +144 -0
- data/lib/ukiryu/cli_commands/resolve_command.rb +124 -0
- data/lib/ukiryu/cli_commands/run_command.rb +38 -14
- data/lib/ukiryu/cli_commands/run_file_command.rb +2 -2
- data/lib/ukiryu/cli_commands/system_command.rb +50 -32
- data/lib/ukiryu/cli_commands/validate_command.rb +452 -51
- data/lib/ukiryu/cli_commands/which_command.rb +5 -5
- data/lib/ukiryu/command_builder.rb +81 -23
- data/lib/ukiryu/config/env_provider.rb +3 -3
- data/lib/ukiryu/config/env_schema.rb +6 -6
- data/lib/ukiryu/config.rb +11 -11
- data/lib/ukiryu/definition/definition_cache.rb +238 -0
- data/lib/ukiryu/definition/definition_composer.rb +257 -0
- data/lib/ukiryu/definition/definition_linter.rb +460 -0
- data/lib/ukiryu/definition/definition_validator.rb +320 -0
- data/lib/ukiryu/definition/discovery.rb +239 -0
- data/lib/ukiryu/definition/documentation_generator.rb +429 -0
- data/lib/ukiryu/definition/lint_issue.rb +168 -0
- data/lib/ukiryu/definition/loader.rb +139 -0
- data/lib/ukiryu/definition/metadata.rb +159 -0
- data/lib/ukiryu/definition/source.rb +87 -0
- data/lib/ukiryu/definition/sources/file.rb +138 -0
- data/lib/ukiryu/definition/sources/string.rb +88 -0
- data/lib/ukiryu/definition/validation_result.rb +158 -0
- data/lib/ukiryu/definition/version_resolver.rb +194 -0
- data/lib/ukiryu/definition.rb +40 -0
- data/lib/ukiryu/errors.rb +6 -0
- data/lib/ukiryu/execution_context.rb +11 -11
- data/lib/ukiryu/executor.rb +6 -0
- data/lib/ukiryu/extractors/extractor.rb +6 -5
- data/lib/ukiryu/extractors/help_parser.rb +13 -19
- data/lib/ukiryu/logger.rb +3 -1
- data/lib/ukiryu/models/command_definition.rb +3 -3
- data/lib/ukiryu/models/command_info.rb +1 -1
- data/lib/ukiryu/models/components.rb +1 -3
- data/lib/ukiryu/models/env_var_definition.rb +11 -3
- data/lib/ukiryu/models/flag_definition.rb +15 -0
- data/lib/ukiryu/models/option_definition.rb +7 -7
- data/lib/ukiryu/models/platform_profile.rb +6 -3
- data/lib/ukiryu/models/routing.rb +1 -1
- data/lib/ukiryu/models/tool_definition.rb +2 -4
- data/lib/ukiryu/models/tool_metadata.rb +6 -6
- data/lib/ukiryu/models/validation_result.rb +1 -1
- data/lib/ukiryu/models/version_compatibility.rb +6 -3
- data/lib/ukiryu/models/version_detection.rb +10 -1
- data/lib/ukiryu/{registry.rb → register.rb} +54 -38
- data/lib/ukiryu/register_auto_manager.rb +268 -0
- data/lib/ukiryu/schema_validator.rb +31 -10
- data/lib/ukiryu/shell/base.rb +18 -0
- data/lib/ukiryu/shell/bash.rb +19 -1
- data/lib/ukiryu/shell/cmd.rb +11 -1
- data/lib/ukiryu/shell/powershell.rb +11 -1
- data/lib/ukiryu/shell.rb +1 -1
- data/lib/ukiryu/tool.rb +107 -95
- data/lib/ukiryu/tool_index.rb +22 -22
- data/lib/ukiryu/tools/base.rb +12 -25
- data/lib/ukiryu/tools/generator.rb +7 -7
- data/lib/ukiryu/tools.rb +3 -3
- data/lib/ukiryu/type.rb +20 -5
- data/lib/ukiryu/version.rb +1 -1
- data/lib/ukiryu/version_detector.rb +21 -2
- data/lib/ukiryu.rb +6 -3
- data/ukiryu-proposal.md +41 -41
- data/ukiryu.gemspec +1 -0
- metadata +64 -8
- 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,
|
|
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
|
-
#
|
|
71
|
-
|
|
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
|
data/lib/ukiryu/shell/base.rb
CHANGED
|
@@ -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
|
data/lib/ukiryu/shell/bash.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
data/lib/ukiryu/shell/cmd.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|