ukiryu 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/docs.yml +63 -0
- data/.github/workflows/links.yml +99 -0
- data/.github/workflows/rake.yml +19 -0
- data/.github/workflows/release.yml +27 -0
- data/.gitignore +18 -4
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +213 -0
- data/Gemfile +12 -8
- data/README.adoc +613 -0
- data/Rakefile +2 -2
- data/docs/assets/logo.svg +1 -0
- data/exe/ukiryu +11 -0
- data/lib/ukiryu/action/base.rb +77 -0
- data/lib/ukiryu/cache.rb +199 -0
- data/lib/ukiryu/cli.rb +133 -307
- data/lib/ukiryu/cli_commands/base_command.rb +155 -0
- data/lib/ukiryu/cli_commands/commands_command.rb +120 -0
- data/lib/ukiryu/cli_commands/commands_command.rb.fixed +40 -0
- data/lib/ukiryu/cli_commands/config_command.rb +249 -0
- data/lib/ukiryu/cli_commands/describe_command.rb +326 -0
- data/lib/ukiryu/cli_commands/describe_command.rb.fixed +254 -0
- data/lib/ukiryu/cli_commands/exec_inline_command.rb.fixed +180 -0
- data/lib/ukiryu/cli_commands/extract_command.rb +84 -0
- data/lib/ukiryu/cli_commands/info_command.rb +156 -0
- data/lib/ukiryu/cli_commands/list_command.rb +70 -0
- data/lib/ukiryu/cli_commands/opts_command.rb +106 -0
- data/lib/ukiryu/cli_commands/opts_command.rb.fixed +105 -0
- data/lib/ukiryu/cli_commands/response_formatter.rb +240 -0
- data/lib/ukiryu/cli_commands/run_command.rb +375 -0
- data/lib/ukiryu/cli_commands/run_file_command.rb +215 -0
- data/lib/ukiryu/cli_commands/system_command.rb +90 -0
- data/lib/ukiryu/cli_commands/validate_command.rb +87 -0
- data/lib/ukiryu/cli_commands/version_command.rb +16 -0
- data/lib/ukiryu/cli_commands/which_command.rb +166 -0
- data/lib/ukiryu/command_builder.rb +205 -0
- data/lib/ukiryu/config/env_provider.rb +64 -0
- data/lib/ukiryu/config/env_schema.rb +63 -0
- data/lib/ukiryu/config/override_resolver.rb +68 -0
- data/lib/ukiryu/config/type_converter.rb +59 -0
- data/lib/ukiryu/config.rb +249 -0
- data/lib/ukiryu/errors.rb +3 -0
- data/lib/ukiryu/executable_locator.rb +114 -0
- data/lib/ukiryu/execution/command_info.rb +64 -0
- data/lib/ukiryu/execution/metadata.rb +97 -0
- data/lib/ukiryu/execution/output.rb +144 -0
- data/lib/ukiryu/execution/result.rb +194 -0
- data/lib/ukiryu/execution.rb +15 -0
- data/lib/ukiryu/execution_context.rb +251 -0
- data/lib/ukiryu/executor.rb +76 -493
- data/lib/ukiryu/extractors/base_extractor.rb +63 -0
- data/lib/ukiryu/extractors/extractor.rb +150 -0
- data/lib/ukiryu/extractors/help_parser.rb +188 -0
- data/lib/ukiryu/extractors/native_extractor.rb +47 -0
- data/lib/ukiryu/io.rb +196 -0
- data/lib/ukiryu/logger.rb +544 -0
- data/lib/ukiryu/models/argument.rb +28 -0
- data/lib/ukiryu/models/argument_definition.rb +119 -0
- data/lib/ukiryu/models/arguments.rb +113 -0
- data/lib/ukiryu/models/command_definition.rb +176 -0
- data/lib/ukiryu/models/command_info.rb +37 -0
- data/lib/ukiryu/models/components.rb +107 -0
- data/lib/ukiryu/models/env_var_definition.rb +30 -0
- data/lib/ukiryu/models/error_response.rb +41 -0
- data/lib/ukiryu/models/execution_metadata.rb +31 -0
- data/lib/ukiryu/models/execution_report.rb +236 -0
- data/lib/ukiryu/models/exit_codes.rb +74 -0
- data/lib/ukiryu/models/flag_definition.rb +67 -0
- data/lib/ukiryu/models/option_definition.rb +102 -0
- data/lib/ukiryu/models/output_info.rb +25 -0
- data/lib/ukiryu/models/platform_profile.rb +153 -0
- data/lib/ukiryu/models/routing.rb +211 -0
- data/lib/ukiryu/models/search_paths.rb +39 -0
- data/lib/ukiryu/models/success_response.rb +85 -0
- data/lib/ukiryu/models/tool_definition.rb +145 -0
- data/lib/ukiryu/models/tool_metadata.rb +82 -0
- data/lib/ukiryu/models/validation_result.rb +80 -0
- data/lib/ukiryu/models/version_compatibility.rb +152 -0
- data/lib/ukiryu/models/version_detection.rb +39 -0
- data/lib/ukiryu/models.rb +23 -0
- data/lib/ukiryu/options/base.rb +95 -0
- data/lib/ukiryu/options_builder/formatter.rb +87 -0
- data/lib/ukiryu/options_builder/validator.rb +43 -0
- data/lib/ukiryu/options_builder.rb +311 -0
- data/lib/ukiryu/platform.rb +6 -6
- data/lib/ukiryu/registry.rb +143 -183
- data/lib/ukiryu/response/base.rb +217 -0
- data/lib/ukiryu/runtime.rb +179 -0
- data/lib/ukiryu/schema_validator.rb +8 -10
- data/lib/ukiryu/shell/bash.rb +3 -3
- data/lib/ukiryu/shell/cmd.rb +4 -4
- data/lib/ukiryu/shell/fish.rb +1 -1
- data/lib/ukiryu/shell/powershell.rb +3 -3
- data/lib/ukiryu/shell/sh.rb +1 -1
- data/lib/ukiryu/shell/zsh.rb +1 -1
- data/lib/ukiryu/shell.rb +146 -39
- data/lib/ukiryu/thor_ext.rb +208 -0
- data/lib/ukiryu/tool.rb +649 -258
- data/lib/ukiryu/tool_index.rb +224 -0
- data/lib/ukiryu/tools/base.rb +381 -0
- data/lib/ukiryu/tools/class_generator.rb +132 -0
- data/lib/ukiryu/tools/executable_finder.rb +29 -0
- data/lib/ukiryu/tools/generator.rb +154 -0
- data/lib/ukiryu/tools.rb +109 -0
- data/lib/ukiryu/type.rb +28 -43
- data/lib/ukiryu/validation/constraints.rb +281 -0
- data/lib/ukiryu/validation/validator.rb +188 -0
- data/lib/ukiryu/validation.rb +21 -0
- data/lib/ukiryu/version.rb +1 -1
- data/lib/ukiryu/version_detector.rb +51 -0
- data/lib/ukiryu.rb +31 -15
- data/ukiryu-proposal.md +2952 -0
- data/ukiryu.gemspec +18 -14
- metadata +137 -5
- data/.github/workflows/test.yml +0 -143
data/ukiryu-proposal.md
ADDED
|
@@ -0,0 +1,2952 @@
|
|
|
1
|
+
# Ukiryu - Platform-Adaptive Command Execution Framework
|
|
2
|
+
|
|
3
|
+
## Project Name: Ukiryu (浮流)
|
|
4
|
+
|
|
5
|
+
**Ukiryu** (pronounced *oo-kee-ryoo*, 浮流) means "floating flow" in Japanese:
|
|
6
|
+
|
|
7
|
+
* **Floating** (浮) - The framework *adapts* to different implementations, platforms, and shells. It doesn't force a single way of working—instead, it flexibly accommodates the unique characteristics of each command-line tool.
|
|
8
|
+
|
|
9
|
+
* **Flow** (流) - The framework *unifies* diverse external tools into a consistent Ruby API. Like streams converging into a river, Ukiryu brings together Inkscape, Ghostscript, ImageMagick, Git, Docker, and countless other tools under one consistent interface.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Executive Summary
|
|
14
|
+
|
|
15
|
+
Ukiryu is a Ruby framework for creating robust, cross-platform wrappers around external command-line tools through declarative YAML profiles. **Ukiryu turns external CLIs into Ruby APIs** with explicit type safety, shell detection, and platform profiles—no hidden magic, no silent fallbacks.
|
|
16
|
+
|
|
17
|
+
### Two-Repository Architecture
|
|
18
|
+
|
|
19
|
+
Ukiryu consists of two separate repositories:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
23
|
+
│ ukiryu/ukiryu (Core Framework) │
|
|
24
|
+
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
25
|
+
│ │ Ruby Gem: Framework Logic │ │
|
|
26
|
+
│ │ │ │
|
|
27
|
+
│ │ - Shell detection (EXPLICIT: bash, zsh, powershell, cmd) │ │
|
|
28
|
+
│ │ - Shell escaping (each shell knows its own rules) │ │
|
|
29
|
+
│ │ - Type validation & conversion │ │
|
|
30
|
+
│ │ - Command execution (timeout, error handling) │ │
|
|
31
|
+
│ │ - Profile selection (EXACT matching on platform+shell+version) │ │
|
|
32
|
+
│ │ - Environment variable management │ │
|
|
33
|
+
│ │ - YAML profile loader │ │
|
|
34
|
+
│ │ - Version detection │ │
|
|
35
|
+
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
36
|
+
│ │
|
|
37
|
+
│ gem: ukiryu │
|
|
38
|
+
│ Zero dependencies (Ruby stdlib only) │
|
|
39
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
40
|
+
│ loads
|
|
41
|
+
▼
|
|
42
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
43
|
+
│ ukiryu/register (Tool Registry) │
|
|
44
|
+
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
|
45
|
+
│ │ YAML Profiles: Tool Definitions │ │
|
|
46
|
+
│ │ │ │
|
|
47
|
+
│ │ tools/inkscape/1.0.yaml tools/ghostscript/10.0.yaml │ │
|
|
48
|
+
│ │ tools/imagemagick/7.0.yaml tools/git/2.45.yaml │ │
|
|
49
|
+
│ │ tools/docker/25.0.yaml tools/ffmpeg/7.0.yaml │ │
|
|
50
|
+
│ │ ... (community-contributed) │ │
|
|
51
|
+
│ │ │
|
|
52
|
+
│ │ schemas/*.yaml.schema (YAML Schema validation) │ │
|
|
53
|
+
│ │ docs/*.adoc (AsciiDoc documentation) │ │
|
|
54
|
+
│ └─────────────────────────────────────────────────────────────────┘ │
|
|
55
|
+
│ │
|
|
56
|
+
│ gem: ukiryu-register (optional, can use as git repo) │
|
|
57
|
+
│ Validates with: json-schema gem │
|
|
58
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Why Two Repositories?
|
|
62
|
+
|
|
63
|
+
| Aspect | Single Repo | Two Repos (Ukiryu) |
|
|
64
|
+
|--------|-----------|-------------------|
|
|
65
|
+
| **Release cycle** | Framework + tools together | Framework independent of tool updates |
|
|
66
|
+
| **Contributors** | Developers only | Anyone can add tools (no Ruby needed) |
|
|
67
|
+
| **Maintenance** | Code changes for new tools | YAML file changes for new tools |
|
|
68
|
+
| **Validation** | Test framework | Validate YAML against schema |
|
|
69
|
+
| **Discovery** | Gem update | `git pull` on registry |
|
|
70
|
+
| **Flexibility** | Coupled | Decoupled - use custom registries |
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## The Vision: Ukiryu as "Reverse Thor" for External Executables
|
|
75
|
+
|
|
76
|
+
### The Thor Model (for context)
|
|
77
|
+
|
|
78
|
+
Thor turns Ruby methods into CLI commands:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
class MyCLI < Thor
|
|
82
|
+
desc "greet NAME", "Say hello"
|
|
83
|
+
option :loud
|
|
84
|
+
def greet(name)
|
|
85
|
+
puts "Hello #{name}!"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Generates CLI:
|
|
90
|
+
# mycli greet Alice --loud
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### The Ukiryu Model (Our Approach)
|
|
94
|
+
|
|
95
|
+
Ukiryu turns external CLI commands into Ruby methods:
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
# Load tool profiles from registry
|
|
99
|
+
Ukiryu::Registry.load_from("ukiryu/register")
|
|
100
|
+
|
|
101
|
+
# Get the tool
|
|
102
|
+
inkscape = Ukiryu::Tool.get("inkscape")
|
|
103
|
+
|
|
104
|
+
# Use like a Ruby API
|
|
105
|
+
inkscape.export(
|
|
106
|
+
inputs: ["diagram.svg"],
|
|
107
|
+
output: "diagram.pdf",
|
|
108
|
+
format: :pdf,
|
|
109
|
+
plain: true
|
|
110
|
+
)
|
|
111
|
+
# CLI: inkscape --export-filename=diagram.pdf --export-type pdf --export-plain-svg diagram.svg
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**The key insight:** Just as Thor provides a DSL to define a CLI, Ukiryu provides a YAML DSL to define a Ruby wrapper around a CLI. Ukiryu becomes the bridge between your Ruby code and external tools, handling all platform differences explicitly.
|
|
115
|
+
|
|
116
|
+
**"Floating"** - Ukiryu adapts to each tool's unique characteristics (different option formats, argument orders, version behaviors).
|
|
117
|
+
|
|
118
|
+
**"Flow"** - Ukiryu unifies diverse tools into one consistent Ruby API.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## The Need
|
|
123
|
+
|
|
124
|
+
### Problem: Four Layers of Cross-Platform Complexity
|
|
125
|
+
|
|
126
|
+
#### Layer 1: Executable Discovery
|
|
127
|
+
|
|
128
|
+
| Platform | Inkscape Binary | Ghostscript Binary |
|
|
129
|
+
|----------|-----------------|-------------------|
|
|
130
|
+
| Windows | `inkscape.exe` | `gswin64c.exe` |
|
|
131
|
+
| macOS | `inkscape` (app bundle) | `gs` |
|
|
132
|
+
| Linux | `inkscape` | `gs` |
|
|
133
|
+
|
|
134
|
+
**Search strategy:**
|
|
135
|
+
- Unix: `ENV["PATH"]` is sufficient (tools installed in standard locations)
|
|
136
|
+
- Windows: `ENV["PATH"]` plus common installation directories
|
|
137
|
+
- `C:/Program Files/Tool/`
|
|
138
|
+
- `C:/Program Files (x86)/Tool/`
|
|
139
|
+
- App bundles on macOS
|
|
140
|
+
|
|
141
|
+
#### Layer 2: Shell Escaping (CRITICAL!)
|
|
142
|
+
|
|
143
|
+
Each shell has fundamentally different escaping rules:
|
|
144
|
+
|
|
145
|
+
**Bash/Zsh:**
|
|
146
|
+
```bash
|
|
147
|
+
# Single quotes: literal (no escaping inside)
|
|
148
|
+
echo 'Hello $USER' # => Hello $USER
|
|
149
|
+
|
|
150
|
+
# Double quotes: variable expansion
|
|
151
|
+
echo "Hello $USER" # => Hello alice
|
|
152
|
+
|
|
153
|
+
# Backslash escaping inside double quotes
|
|
154
|
+
echo "Path: \"file\"" # => Path: "file"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**PowerShell:**
|
|
158
|
+
```powershell
|
|
159
|
+
# Single quotes: literal
|
|
160
|
+
Write-Host 'Hello $ENV:USER' # => Hello $ENV:USER
|
|
161
|
+
|
|
162
|
+
# Double quotes: variable expansion
|
|
163
|
+
Write-Host "Hello $ENV:USER" # => Hello alice
|
|
164
|
+
|
|
165
|
+
# Backtick escaping inside double quotes
|
|
166
|
+
Write-Host "Path: `"` # => Path: "
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**cmd.exe:**
|
|
170
|
+
```cmd
|
|
171
|
+
# Caret is escape character
|
|
172
|
+
echo ^^%USERNAME%^ # alice (caret escapes, % expands)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### Layer 3: Command Syntax Differences
|
|
176
|
+
|
|
177
|
+
| Tool Aspect | Unix | Windows (some ports) |
|
|
178
|
+
|------------|------|-------------------|
|
|
179
|
+
| Long options | `--option=value` | `/Option:value` |
|
|
180
|
+
| Short options | `-o value` | `/Ovalue` |
|
|
181
|
+
| Output specification | `--output=file.pdf` | `/Output:file.pdf` |
|
|
182
|
+
| Flags | `--plain` | `/plain` |
|
|
183
|
+
| Path format | `/usr/bin/file` | `C:\Program Files\file` |
|
|
184
|
+
|
|
185
|
+
#### Layer 4: Type Safety & Validation
|
|
186
|
+
|
|
187
|
+
| Issue | Example | Consequence |
|
|
188
|
+
|-------|---------|--------------|
|
|
189
|
+
| Wrong shell escaping | Unescaped `$USER` | Security vulnerability |
|
|
190
|
+
| Wrong path for platform | `/usr/bin/file` on Windows | "file not found" |
|
|
191
|
+
| Invalid enum value | `format: :docx` | Tool fails with cryptic error |
|
|
192
|
+
| Out-of-range integer | `quality: 150` | Tool uses default or fails |
|
|
193
|
+
|
|
194
|
+
### Current Solutions Are Insufficient
|
|
195
|
+
|
|
196
|
+
| Solution | Problems |
|
|
197
|
+
|----------|----------|
|
|
198
|
+
| `Open3.capture3` | No types, no escaping, no platform awareness |
|
|
199
|
+
| `Shellwords.escape` | Bash-only, doesn't handle cmd/PowerShell |
|
|
200
|
+
| `posix-spawn` | Unix-only |
|
|
201
|
+
| **Each library reimplements everything poorly** | Incomplete, inconsistent, error-prone |
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## What Ukiryu Does
|
|
206
|
+
|
|
207
|
+
Ukiryu provides a complete execution pipeline with **explicit** (no fallbacks):
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
Ruby API Call (Typed Parameters)
|
|
211
|
+
↓
|
|
212
|
+
Type Validation (Semantic: paths, URIs, ranges, enums)
|
|
213
|
+
↓ (Error if invalid)
|
|
214
|
+
Shell Detection (EXPLICIT or raise error)
|
|
215
|
+
↓ (Error if unknown)
|
|
216
|
+
Profile Selection (EXACT match on platform+shell+version)
|
|
217
|
+
↓ (Error if no match)
|
|
218
|
+
Command Building (Shell-specific escaping)
|
|
219
|
+
↓
|
|
220
|
+
Execution (Platform-specific methods)
|
|
221
|
+
↓
|
|
222
|
+
Result Parsing
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Key principle:** Explicit over implicit. If Ukiryu can't determine shell or profile, it raises a clear error rather than guessing.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Architecture: Hybrid Approach (Ruby Framework + YAML Profiles)
|
|
230
|
+
|
|
231
|
+
### Two-Layer Design
|
|
232
|
+
|
|
233
|
+
Ukiryu separates **framework logic** (Ruby) from **tool definitions** (YAML):
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
237
|
+
│ UKIRYU FRAMEWORK (Ruby) │
|
|
238
|
+
│ - Shell detection & escaping │
|
|
239
|
+
│ - Type validation & conversion │
|
|
240
|
+
│ - Execution & timeout handling │
|
|
241
|
+
│ - Profile selection algorithm │
|
|
242
|
+
│ - Environment variable management │
|
|
243
|
+
└───────────────────────────┬─────────────────────────────────┘
|
|
244
|
+
│ loads
|
|
245
|
+
▼
|
|
246
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
247
|
+
│ TOOL PROFILES (YAML Registry) │
|
|
248
|
+
│ - inkscape.yaml (all versions) │
|
|
249
|
+
│ - ghostscript.yaml (all versions) │
|
|
250
|
+
│ - imagemagick.yaml (all versions) │
|
|
251
|
+
│ - git.yaml (all subcommands) │
|
|
252
|
+
│ - ... (community-contributed) │
|
|
253
|
+
└─────────────────────────────────────────────────────────────┘
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Why YAML Profiles?
|
|
257
|
+
|
|
258
|
+
| Aspect | Ruby DSL | YAML Profiles |
|
|
259
|
+
|--------|----------|---------------|
|
|
260
|
+
| **New tool support** | Requires code + release | Add YAML file |
|
|
261
|
+
| **Version update** | Requires code + release | Update YAML |
|
|
262
|
+
| **Maintenance** | Developers only | Anyone can edit |
|
|
263
|
+
| **Validation** | Compile-time | Load-time |
|
|
264
|
+
| **Distribution** | Gem releases | Separate registry |
|
|
265
|
+
| **Community** | Pull requests | PRs to registry repo |
|
|
266
|
+
|
|
267
|
+
### YAML Profile Structure
|
|
268
|
+
|
|
269
|
+
```yaml
|
|
270
|
+
# profiles/inkscape.yaml
|
|
271
|
+
|
|
272
|
+
name: inkscape
|
|
273
|
+
aliases:
|
|
274
|
+
- inkscapecom
|
|
275
|
+
- ink
|
|
276
|
+
|
|
277
|
+
version_detection:
|
|
278
|
+
command: "--version"
|
|
279
|
+
pattern: "Inkscape (\\d+\\.\\d+)"
|
|
280
|
+
modern_threshold: "1.0"
|
|
281
|
+
|
|
282
|
+
search_paths:
|
|
283
|
+
windows:
|
|
284
|
+
- "C:/Program Files/Inkscape*/inkscape.exe"
|
|
285
|
+
macos:
|
|
286
|
+
- "/Applications/Inkscape.app/Contents/MacOS/inkscape"
|
|
287
|
+
# Unix: rely on PATH only
|
|
288
|
+
|
|
289
|
+
profiles:
|
|
290
|
+
# Modern Inkscape (1.0+) on Unix
|
|
291
|
+
- name: modern_unix
|
|
292
|
+
platforms: [macos, linux]
|
|
293
|
+
shells: [bash, zsh, fish, sh]
|
|
294
|
+
version: ">= 1.0"
|
|
295
|
+
option_style: double_dash_equals
|
|
296
|
+
commands:
|
|
297
|
+
export:
|
|
298
|
+
arguments:
|
|
299
|
+
- name: inputs
|
|
300
|
+
type: file
|
|
301
|
+
variadic: true
|
|
302
|
+
position: last
|
|
303
|
+
min: 1
|
|
304
|
+
|
|
305
|
+
options:
|
|
306
|
+
- name: output
|
|
307
|
+
type: file
|
|
308
|
+
cli: "--export-filename="
|
|
309
|
+
format: double_dash_equals
|
|
310
|
+
|
|
311
|
+
- name: format
|
|
312
|
+
type: symbol
|
|
313
|
+
values: [svg, png, ps, eps, pdf, emf, wmf, xaml]
|
|
314
|
+
cli: "--export-type"
|
|
315
|
+
format: double_dash_space
|
|
316
|
+
separator: " "
|
|
317
|
+
|
|
318
|
+
- name: dpi
|
|
319
|
+
type: integer
|
|
320
|
+
cli: "-d"
|
|
321
|
+
format: single_dash_space
|
|
322
|
+
separator: " "
|
|
323
|
+
|
|
324
|
+
- name: quality
|
|
325
|
+
type: integer
|
|
326
|
+
cli: "-d"
|
|
327
|
+
range: [0, 100]
|
|
328
|
+
|
|
329
|
+
- name: export_background
|
|
330
|
+
type: string
|
|
331
|
+
cli: "--export-background="
|
|
332
|
+
|
|
333
|
+
flags:
|
|
334
|
+
- name: plain
|
|
335
|
+
cli: "--export-plain-svg"
|
|
336
|
+
cli_short: "-l"
|
|
337
|
+
|
|
338
|
+
- name: export_text_to_path
|
|
339
|
+
cli: "--export-text-to-path"
|
|
340
|
+
cli_short: "-T"
|
|
341
|
+
|
|
342
|
+
env_vars:
|
|
343
|
+
- name: DISPLAY
|
|
344
|
+
value: ""
|
|
345
|
+
platforms: [macos, linux]
|
|
346
|
+
|
|
347
|
+
query:
|
|
348
|
+
arguments:
|
|
349
|
+
- name: input
|
|
350
|
+
type: file
|
|
351
|
+
required: true
|
|
352
|
+
|
|
353
|
+
flags:
|
|
354
|
+
- name: width
|
|
355
|
+
cli: "--query-width"
|
|
356
|
+
cli_short: "-W"
|
|
357
|
+
|
|
358
|
+
- name: height
|
|
359
|
+
cli: "--query-height"
|
|
360
|
+
cli_short: "-H"
|
|
361
|
+
|
|
362
|
+
- name: x
|
|
363
|
+
cli: "--query-x"
|
|
364
|
+
cli_short: "-X"
|
|
365
|
+
|
|
366
|
+
- name: y
|
|
367
|
+
cli: "--query-y"
|
|
368
|
+
cli_short: "-Y"
|
|
369
|
+
|
|
370
|
+
# Modern Inkscape (1.0+) on Windows PowerShell
|
|
371
|
+
- name: modern_windows_powershell
|
|
372
|
+
platforms: [windows]
|
|
373
|
+
shells: [powershell]
|
|
374
|
+
version: ">= 1.0"
|
|
375
|
+
option_style: double_dash_equals
|
|
376
|
+
# Same command structure as modern_unix
|
|
377
|
+
# (inherits from base profile)
|
|
378
|
+
inherits: modern_unix
|
|
379
|
+
|
|
380
|
+
# Modern Inkscape (1.0+) on Windows cmd
|
|
381
|
+
- name: modern_windows_cmd
|
|
382
|
+
platforms: [windows]
|
|
383
|
+
shells: [cmd]
|
|
384
|
+
version: ">= 1.0"
|
|
385
|
+
option_style: slash_space
|
|
386
|
+
commands:
|
|
387
|
+
export:
|
|
388
|
+
# Same structure but different CLI syntax
|
|
389
|
+
options:
|
|
390
|
+
- name: output
|
|
391
|
+
type: file
|
|
392
|
+
cli: "/ExportFilename"
|
|
393
|
+
format: slash_space
|
|
394
|
+
separator: " "
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### YAML Profile Registry
|
|
398
|
+
|
|
399
|
+
**Registry structure:**
|
|
400
|
+
```
|
|
401
|
+
ukiryu-register/
|
|
402
|
+
├── tools/
|
|
403
|
+
│ ├── inkscape/
|
|
404
|
+
│ │ ├── 1.0.yaml
|
|
405
|
+
│ │ ├── 0.92.yaml
|
|
406
|
+
│ │ └── 0.9.yaml
|
|
407
|
+
│ ├── ghostscript/
|
|
408
|
+
│ │ ├── 10.0.yaml
|
|
409
|
+
│ │ ├── 9.5.yaml
|
|
410
|
+
│ │ └── 9.0.yaml
|
|
411
|
+
│ ├── imagemagick/
|
|
412
|
+
│ │ ├── 7.0.yaml
|
|
413
|
+
│ │ └── 6.0.yaml
|
|
414
|
+
│ ├── git/
|
|
415
|
+
│ │ ├── 2.45.yaml
|
|
416
|
+
│ │ └── 2.40.yaml
|
|
417
|
+
│ ├── docker/
|
|
418
|
+
│ │ ├── 25.0.yaml
|
|
419
|
+
│ │ └── 24.0.yaml
|
|
420
|
+
│ └── ...
|
|
421
|
+
├── schemas/
|
|
422
|
+
│ ├── tool-profile.yaml.schema
|
|
423
|
+
│ ├── command-definition.yaml.schema
|
|
424
|
+
│ └── registry.yaml.schema
|
|
425
|
+
├── docs/
|
|
426
|
+
│ ├── inkscape.adoc
|
|
427
|
+
│ ├── ghostscript.adoc
|
|
428
|
+
│ ├── contributing.adoc
|
|
429
|
+
│ └── registry.adoc
|
|
430
|
+
├── lib/
|
|
431
|
+
│ └── ukiryu/
|
|
432
|
+
│ └── registry.rb # Registry helper library
|
|
433
|
+
├── Gemfile # json-schema gem
|
|
434
|
+
├── Rakefile # Validation tasks
|
|
435
|
+
└── README.adoc
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**Version file naming:**
|
|
439
|
+
- Use semantic version: `1.0.yaml`, `0.9.5.yaml`
|
|
440
|
+
- Multiple profiles per version (platform/shell combos) in one file
|
|
441
|
+
- Registry selects newest compatible version
|
|
442
|
+
|
|
443
|
+
**Loading profiles in Ukiryu:**
|
|
444
|
+
```ruby
|
|
445
|
+
# Load from registry
|
|
446
|
+
Ukiryu::Registry.load_from("/path/to/ukiryu/register")
|
|
447
|
+
|
|
448
|
+
# Or load from gem-builtin profiles
|
|
449
|
+
Ukiryu::Registry.load_builtins
|
|
450
|
+
|
|
451
|
+
# Load specific tool/version
|
|
452
|
+
Ukiryu::Registry.load_tool("inkscape", version: "1.0")
|
|
453
|
+
Ukiryu::Registry.load_tool("inkscape") # Auto-detect latest
|
|
454
|
+
|
|
455
|
+
# Use the tool
|
|
456
|
+
inkscape = Ukiryu::Tool.get("inkscape")
|
|
457
|
+
inkscape.export(
|
|
458
|
+
inputs: ["diagram.svg"],
|
|
459
|
+
output: "diagram.pdf",
|
|
460
|
+
format: :pdf
|
|
461
|
+
)
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Schema validation with `json-schema` gem:**
|
|
465
|
+
```ruby
|
|
466
|
+
# In registry Rakefile
|
|
467
|
+
require 'json-schema'
|
|
468
|
+
|
|
469
|
+
namespace :validate do
|
|
470
|
+
task :all do
|
|
471
|
+
# Validate all YAML files against schemas
|
|
472
|
+
Dir.glob("tools/*/*.yaml").each do |file|
|
|
473
|
+
schema = JSON::Schema.new(parse_schema("schemas/tool-profile.yaml.schema"))
|
|
474
|
+
schema.validate(YAML.load_file(file))
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Profile Inheritance
|
|
481
|
+
|
|
482
|
+
Avoid duplication with profile inheritance:
|
|
483
|
+
|
|
484
|
+
```yaml
|
|
485
|
+
# ghostscript/10.0.yaml
|
|
486
|
+
name: ghostscript
|
|
487
|
+
version: "10.0"
|
|
488
|
+
display_name: Ghostscript 10.0
|
|
489
|
+
homepage: https://www.ghostscript.com/
|
|
490
|
+
|
|
491
|
+
profiles:
|
|
492
|
+
# Base Ghostscript profile
|
|
493
|
+
- name: base_ghostscript
|
|
494
|
+
commands:
|
|
495
|
+
convert:
|
|
496
|
+
options:
|
|
497
|
+
- name: device
|
|
498
|
+
type: symbol
|
|
499
|
+
values: [pdfwrite, eps2write, png16m, jpeg]
|
|
500
|
+
cli: "-sDEVICE="
|
|
501
|
+
|
|
502
|
+
- name: output
|
|
503
|
+
type: file
|
|
504
|
+
cli: "-sOutputFile="
|
|
505
|
+
|
|
506
|
+
flags:
|
|
507
|
+
- name: safer
|
|
508
|
+
cli: "-dSAFER"
|
|
509
|
+
|
|
510
|
+
- name: quiet
|
|
511
|
+
cli: "-q"
|
|
512
|
+
|
|
513
|
+
# Unix variant
|
|
514
|
+
- name: unix
|
|
515
|
+
platforms: [macos, linux]
|
|
516
|
+
shells: [bash, zsh, fish, sh]
|
|
517
|
+
inherits: base_ghostscript
|
|
518
|
+
# Add Unix-specific options
|
|
519
|
+
commands:
|
|
520
|
+
convert:
|
|
521
|
+
options:
|
|
522
|
+
- name: lib_paths
|
|
523
|
+
type: array
|
|
524
|
+
of: file
|
|
525
|
+
cli: "-I"
|
|
526
|
+
separator: " "
|
|
527
|
+
|
|
528
|
+
# Windows variant
|
|
529
|
+
- name: windows
|
|
530
|
+
platforms: [windows]
|
|
531
|
+
shells: [powershell, cmd]
|
|
532
|
+
inherits: base_ghostscript
|
|
533
|
+
# Same structure, shell handles escaping
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Real-World: Inkscape Complete YAML Profile
|
|
537
|
+
|
|
538
|
+
```yaml
|
|
539
|
+
# ukiryu-register/tools/inkscape.yaml
|
|
540
|
+
|
|
541
|
+
name: inkscape
|
|
542
|
+
display_name: Inkscape Vector Graphics Editor
|
|
543
|
+
homepage: https://inkscape.org/
|
|
544
|
+
version_detection:
|
|
545
|
+
command: "--version"
|
|
546
|
+
pattern: "Inkscape (\\d+\\.\\d+)"
|
|
547
|
+
modern_threshold: "1.0"
|
|
548
|
+
|
|
549
|
+
search_paths:
|
|
550
|
+
windows:
|
|
551
|
+
- "C:/Program Files/Inkscape*/inkscape.exe"
|
|
552
|
+
- "C:/Program Files (x86)/Inkscape*/inkscape.exe"
|
|
553
|
+
macos:
|
|
554
|
+
- "/Applications/Inkscape.app/Contents/MacOS/inkscape"
|
|
555
|
+
# Unix: PATH only, no hardcoded paths
|
|
556
|
+
|
|
557
|
+
aliases: [inkscapecom, ink]
|
|
558
|
+
|
|
559
|
+
timeout: 90
|
|
560
|
+
terminate_signal: TERM
|
|
561
|
+
|
|
562
|
+
profiles:
|
|
563
|
+
# ============================================
|
|
564
|
+
# Modern Inkscape (1.0+) - Unix
|
|
565
|
+
# ============================================
|
|
566
|
+
- name: modern_unix
|
|
567
|
+
display_name: Modern Inkscape on Unix
|
|
568
|
+
platforms: [macos, linux]
|
|
569
|
+
shells: [bash, zsh, fish, sh]
|
|
570
|
+
version: ">= 1.0"
|
|
571
|
+
option_style: double_dash_equals
|
|
572
|
+
escape_quotes: single
|
|
573
|
+
|
|
574
|
+
commands:
|
|
575
|
+
# ============================================
|
|
576
|
+
# COMMAND: export
|
|
577
|
+
# ============================================
|
|
578
|
+
export:
|
|
579
|
+
description: "Export document to different format"
|
|
580
|
+
usage: "inkscape [OPTIONS] input1.svg [input2.svg ...]"
|
|
581
|
+
|
|
582
|
+
arguments:
|
|
583
|
+
- name: inputs
|
|
584
|
+
type: file
|
|
585
|
+
variadic: true
|
|
586
|
+
position: last
|
|
587
|
+
min: 1
|
|
588
|
+
description: "Input file(s)"
|
|
589
|
+
|
|
590
|
+
options:
|
|
591
|
+
# Output file
|
|
592
|
+
- name: output
|
|
593
|
+
type: file
|
|
594
|
+
cli: "--export-filename="
|
|
595
|
+
format: double_dash_equals
|
|
596
|
+
description: "Output filename"
|
|
597
|
+
required: true
|
|
598
|
+
|
|
599
|
+
# Export format
|
|
600
|
+
- name: format
|
|
601
|
+
type: symbol
|
|
602
|
+
cli: "--export-type"
|
|
603
|
+
format: double_dash_space
|
|
604
|
+
separator: " "
|
|
605
|
+
values: [svg, png, ps, eps, pdf, emf, wmf, xaml]
|
|
606
|
+
description: "Output format"
|
|
607
|
+
|
|
608
|
+
# Multiple export types (comma-separated)
|
|
609
|
+
- name: export_types
|
|
610
|
+
type: array
|
|
611
|
+
of: symbol
|
|
612
|
+
cli: "--export-type="
|
|
613
|
+
separator: ","
|
|
614
|
+
values: [svg, png, ps, eps, pdf, emf, wmf, xaml]
|
|
615
|
+
description: "Multiple export formats"
|
|
616
|
+
|
|
617
|
+
# DPI for bitmap export
|
|
618
|
+
- name: dpi
|
|
619
|
+
type: integer
|
|
620
|
+
cli: "-d"
|
|
621
|
+
format: single_dash_space
|
|
622
|
+
separator: " "
|
|
623
|
+
range: [1, 10000]
|
|
624
|
+
description: "Resolution for bitmaps (default: 96)"
|
|
625
|
+
|
|
626
|
+
# Width
|
|
627
|
+
- name: width
|
|
628
|
+
type: integer
|
|
629
|
+
cli: "--export-width="
|
|
630
|
+
description: "Bitmap width in pixels"
|
|
631
|
+
|
|
632
|
+
# Height
|
|
633
|
+
- name: height
|
|
634
|
+
type: integer
|
|
635
|
+
cli: "--export-height="
|
|
636
|
+
description: "Bitmap height in pixels"
|
|
637
|
+
|
|
638
|
+
# Export area
|
|
639
|
+
- name: area
|
|
640
|
+
type: string
|
|
641
|
+
cli: "--export-area="
|
|
642
|
+
description: "Export area (x0:y0:x1:y1)"
|
|
643
|
+
|
|
644
|
+
# Export page
|
|
645
|
+
- name: export_page
|
|
646
|
+
type: string
|
|
647
|
+
cli: "--export-page="
|
|
648
|
+
description: "Page number to export"
|
|
649
|
+
|
|
650
|
+
# Background color
|
|
651
|
+
- name: background
|
|
652
|
+
type: string
|
|
653
|
+
cli: "--export-background="
|
|
654
|
+
description: "Background color"
|
|
655
|
+
|
|
656
|
+
# Background opacity
|
|
657
|
+
- name: background_opacity
|
|
658
|
+
type: float
|
|
659
|
+
cli: "--export-background-opacity="
|
|
660
|
+
range: [0.0, 1.0]
|
|
661
|
+
description: "Background opacity (0.0 to 1.0)"
|
|
662
|
+
|
|
663
|
+
# Object IDs to export (semicolon-separated)
|
|
664
|
+
- name: export_ids
|
|
665
|
+
type: array
|
|
666
|
+
of: string
|
|
667
|
+
cli: "--export-id="
|
|
668
|
+
separator: ";"
|
|
669
|
+
description: "Object IDs to export"
|
|
670
|
+
|
|
671
|
+
flags:
|
|
672
|
+
# Plain SVG export
|
|
673
|
+
- name: plain
|
|
674
|
+
cli: "--export-plain-svg"
|
|
675
|
+
cli_short: "-l"
|
|
676
|
+
description: "Remove Inkscape-specific attributes"
|
|
677
|
+
|
|
678
|
+
# Export area = page
|
|
679
|
+
- name: area_page
|
|
680
|
+
cli: "--export-area-page"
|
|
681
|
+
cli_short: "-C"
|
|
682
|
+
description: "Export area is page"
|
|
683
|
+
|
|
684
|
+
# Export area = drawing
|
|
685
|
+
- name: area_drawing
|
|
686
|
+
cli: "--export-area-drawing"
|
|
687
|
+
cli_short: "-D"
|
|
688
|
+
description: "Export area is drawing"
|
|
689
|
+
|
|
690
|
+
# Text to path
|
|
691
|
+
- name: text_to_path
|
|
692
|
+
cli: "--export-text-to-path"
|
|
693
|
+
cli_short: "-T"
|
|
694
|
+
description: "Convert text to paths"
|
|
695
|
+
|
|
696
|
+
# Ignore filters
|
|
697
|
+
- name: ignore_filters
|
|
698
|
+
cli: "--export-ignore-filters"
|
|
699
|
+
description: "Render without filters"
|
|
700
|
+
|
|
701
|
+
# Vacuum defs
|
|
702
|
+
- name: vacuum_defs
|
|
703
|
+
cli: "--vacuum-defs"
|
|
704
|
+
description: "Remove unused definitions"
|
|
705
|
+
|
|
706
|
+
env_vars:
|
|
707
|
+
- name: DISPLAY
|
|
708
|
+
value: ""
|
|
709
|
+
platforms: [macos, linux]
|
|
710
|
+
description: "Disable display for headless operation"
|
|
711
|
+
|
|
712
|
+
# ============================================
|
|
713
|
+
# COMMAND: query
|
|
714
|
+
# ============================================
|
|
715
|
+
query:
|
|
716
|
+
description: "Query document dimensions"
|
|
717
|
+
usage: "inkscape --query-width/--query-height input.svg"
|
|
718
|
+
|
|
719
|
+
arguments:
|
|
720
|
+
- name: input
|
|
721
|
+
type: file
|
|
722
|
+
required: true
|
|
723
|
+
description: "Input file"
|
|
724
|
+
|
|
725
|
+
flags:
|
|
726
|
+
- name: width
|
|
727
|
+
cli: "--query-width"
|
|
728
|
+
cli_short: "-W"
|
|
729
|
+
description: "Query width"
|
|
730
|
+
|
|
731
|
+
- name: height
|
|
732
|
+
cli: "--query-height"
|
|
733
|
+
cli_short: "-H"
|
|
734
|
+
description: "Query height"
|
|
735
|
+
|
|
736
|
+
- name: x
|
|
737
|
+
cli: "--query-x"
|
|
738
|
+
cli_short: "-X"
|
|
739
|
+
description: "Query X coordinate"
|
|
740
|
+
|
|
741
|
+
- name: y
|
|
742
|
+
cli: "--query-y"
|
|
743
|
+
cli_short: "-Y"
|
|
744
|
+
description: "Query Y coordinate"
|
|
745
|
+
|
|
746
|
+
parse_output:
|
|
747
|
+
type: hash
|
|
748
|
+
pattern: "key:\\s*value"
|
|
749
|
+
|
|
750
|
+
# ============================================
|
|
751
|
+
# Modern Inkscape (1.0+) - Windows PowerShell
|
|
752
|
+
# ============================================
|
|
753
|
+
- name: modern_windows_powershell
|
|
754
|
+
display_name: Modern Inkscape on Windows (PowerShell)
|
|
755
|
+
platforms: [windows]
|
|
756
|
+
shells: [powershell]
|
|
757
|
+
version: ">= 1.0"
|
|
758
|
+
option_style: double_dash_equals
|
|
759
|
+
inherits: modern_unix
|
|
760
|
+
# Inherits all commands, shell handles escaping
|
|
761
|
+
|
|
762
|
+
# ============================================
|
|
763
|
+
# Modern Inkscape (1.0+) - Windows cmd
|
|
764
|
+
# ============================================
|
|
765
|
+
- name: modern_windows_cmd
|
|
766
|
+
display_name: Modern Inkscape on Windows (cmd)
|
|
767
|
+
platforms: [windows]
|
|
768
|
+
shells: [cmd]
|
|
769
|
+
version: ">= 1.0"
|
|
770
|
+
option_style: slash_space
|
|
771
|
+
# Need to redefine commands for different option syntax
|
|
772
|
+
commands:
|
|
773
|
+
export:
|
|
774
|
+
arguments:
|
|
775
|
+
- name: inputs
|
|
776
|
+
type: file
|
|
777
|
+
variadic: true
|
|
778
|
+
position: last
|
|
779
|
+
min: 1
|
|
780
|
+
|
|
781
|
+
options:
|
|
782
|
+
- name: output
|
|
783
|
+
type: file
|
|
784
|
+
cli: "/ExportFilename"
|
|
785
|
+
format: slash_space
|
|
786
|
+
separator: " "
|
|
787
|
+
|
|
788
|
+
# ... other options with /Option syntax
|
|
789
|
+
|
|
790
|
+
flags:
|
|
791
|
+
- name: plain
|
|
792
|
+
cli: "/Plain"
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
### Schema Validation
|
|
796
|
+
|
|
797
|
+
YAML profiles are validated against YAML Schema using the `json-schema` gem:
|
|
798
|
+
|
|
799
|
+
```yaml
|
|
800
|
+
# schemas/tool-profile.yaml.schema
|
|
801
|
+
---
|
|
802
|
+
$schema: "http://json-schema.org/draft-07/schema#"
|
|
803
|
+
$title: "Ukiryu Tool Profile"
|
|
804
|
+
$description: "Schema for Ukiryu tool command profiles"
|
|
805
|
+
$type: "object"
|
|
806
|
+
$required:
|
|
807
|
+
- name
|
|
808
|
+
- version
|
|
809
|
+
- display_name
|
|
810
|
+
- profiles
|
|
811
|
+
|
|
812
|
+
properties:
|
|
813
|
+
name:
|
|
814
|
+
type: string
|
|
815
|
+
description: "Tool command name (e.g., 'inkscape', 'gs')"
|
|
816
|
+
|
|
817
|
+
version:
|
|
818
|
+
type: string
|
|
819
|
+
description: "Tool semantic version (e.g., '1.0', '10.0.0')"
|
|
820
|
+
|
|
821
|
+
display_name:
|
|
822
|
+
type: string
|
|
823
|
+
description: "Human-readable tool name"
|
|
824
|
+
|
|
825
|
+
homepage:
|
|
826
|
+
type: string
|
|
827
|
+
format: uri
|
|
828
|
+
description: "Tool homepage URL"
|
|
829
|
+
|
|
830
|
+
aliases:
|
|
831
|
+
type: array
|
|
832
|
+
items:
|
|
833
|
+
type: string
|
|
834
|
+
description: "Alternative command names"
|
|
835
|
+
|
|
836
|
+
search_paths:
|
|
837
|
+
type: object
|
|
838
|
+
properties:
|
|
839
|
+
windows:
|
|
840
|
+
type: array
|
|
841
|
+
items:
|
|
842
|
+
type: string
|
|
843
|
+
macos:
|
|
844
|
+
type: array
|
|
845
|
+
items:
|
|
846
|
+
type: string
|
|
847
|
+
# Unix: no hardcoded paths, rely on PATH
|
|
848
|
+
|
|
849
|
+
version_detection:
|
|
850
|
+
type: object
|
|
851
|
+
$required:
|
|
852
|
+
- command
|
|
853
|
+
- pattern
|
|
854
|
+
properties:
|
|
855
|
+
command:
|
|
856
|
+
type: string
|
|
857
|
+
pattern:
|
|
858
|
+
type: string
|
|
859
|
+
modern_threshold:
|
|
860
|
+
type: string
|
|
861
|
+
|
|
862
|
+
profiles:
|
|
863
|
+
type: array
|
|
864
|
+
items:
|
|
865
|
+
type: object
|
|
866
|
+
$required:
|
|
867
|
+
- name
|
|
868
|
+
- platforms
|
|
869
|
+
- shells
|
|
870
|
+
- commands
|
|
871
|
+
properties:
|
|
872
|
+
name:
|
|
873
|
+
type: string
|
|
874
|
+
|
|
875
|
+
platforms:
|
|
876
|
+
type: array
|
|
877
|
+
items:
|
|
878
|
+
type: string
|
|
879
|
+
enum: [windows, macos, linux]
|
|
880
|
+
|
|
881
|
+
shells:
|
|
882
|
+
type: array
|
|
883
|
+
items:
|
|
884
|
+
type: string
|
|
885
|
+
enum: [bash, zsh, fish, sh, powershell, cmd]
|
|
886
|
+
|
|
887
|
+
version:
|
|
888
|
+
type: string
|
|
889
|
+
description: "Version constraint (e.g., '>= 1.0', '< 2.0')"
|
|
890
|
+
|
|
891
|
+
inherits:
|
|
892
|
+
type: string
|
|
893
|
+
description: "Profile name to inherit from"
|
|
894
|
+
|
|
895
|
+
option_style:
|
|
896
|
+
type: string
|
|
897
|
+
enum: [double_dash_equals, double_dash_space,
|
|
898
|
+
single_dash_equals, single_dash_space,
|
|
899
|
+
slash_space, slash_colon]
|
|
900
|
+
|
|
901
|
+
commands:
|
|
902
|
+
type: object
|
|
903
|
+
patternProperties:
|
|
904
|
+
"^[a-z_]+$":
|
|
905
|
+
type: object
|
|
906
|
+
$required:
|
|
907
|
+
- arguments
|
|
908
|
+
properties:
|
|
909
|
+
description:
|
|
910
|
+
type: string
|
|
911
|
+
|
|
912
|
+
usage:
|
|
913
|
+
type: string
|
|
914
|
+
|
|
915
|
+
arguments:
|
|
916
|
+
type: array
|
|
917
|
+
items:
|
|
918
|
+
type: object
|
|
919
|
+
$required:
|
|
920
|
+
- name
|
|
921
|
+
- type
|
|
922
|
+
properties:
|
|
923
|
+
name:
|
|
924
|
+
type: string
|
|
925
|
+
|
|
926
|
+
type:
|
|
927
|
+
type: string
|
|
928
|
+
enum: [file, string, integer, float, symbol, boolean, uri, datetime, hash, array]
|
|
929
|
+
|
|
930
|
+
variadic:
|
|
931
|
+
type: boolean
|
|
932
|
+
|
|
933
|
+
min:
|
|
934
|
+
type: integer
|
|
935
|
+
|
|
936
|
+
position:
|
|
937
|
+
oneOf:
|
|
938
|
+
- type: integer
|
|
939
|
+
- type: string
|
|
940
|
+
enum: [last]
|
|
941
|
+
|
|
942
|
+
required:
|
|
943
|
+
type: boolean
|
|
944
|
+
|
|
945
|
+
values:
|
|
946
|
+
type: array
|
|
947
|
+
items:
|
|
948
|
+
type: string
|
|
949
|
+
|
|
950
|
+
range:
|
|
951
|
+
type: array
|
|
952
|
+
items:
|
|
953
|
+
type: number
|
|
954
|
+
minItems: 2
|
|
955
|
+
maxItems: 2
|
|
956
|
+
|
|
957
|
+
separator:
|
|
958
|
+
type: string
|
|
959
|
+
|
|
960
|
+
default:
|
|
961
|
+
type: boolean
|
|
962
|
+
|
|
963
|
+
description:
|
|
964
|
+
type: string
|
|
965
|
+
|
|
966
|
+
options:
|
|
967
|
+
type: array
|
|
968
|
+
items:
|
|
969
|
+
type: object
|
|
970
|
+
$required:
|
|
971
|
+
- name
|
|
972
|
+
- cli
|
|
973
|
+
properties:
|
|
974
|
+
name:
|
|
975
|
+
type: string
|
|
976
|
+
|
|
977
|
+
type:
|
|
978
|
+
type: string
|
|
979
|
+
enum: [file, string, integer, float, symbol, boolean, uri, datetime, hash, array]
|
|
980
|
+
|
|
981
|
+
cli:
|
|
982
|
+
type: string
|
|
983
|
+
|
|
984
|
+
cli_short:
|
|
985
|
+
type: string
|
|
986
|
+
|
|
987
|
+
format:
|
|
988
|
+
type: string
|
|
989
|
+
enum: [double_dash_equals, double_dash_space,
|
|
990
|
+
single_dash_equals, single_dash_space,
|
|
991
|
+
slash_space, slash_colon]
|
|
992
|
+
|
|
993
|
+
separator:
|
|
994
|
+
type: string
|
|
995
|
+
|
|
996
|
+
values:
|
|
997
|
+
type: array
|
|
998
|
+
items:
|
|
999
|
+
type: string
|
|
1000
|
+
|
|
1001
|
+
range:
|
|
1002
|
+
type: array
|
|
1003
|
+
items:
|
|
1004
|
+
type: number
|
|
1005
|
+
minItems: 2
|
|
1006
|
+
maxItems: 2
|
|
1007
|
+
|
|
1008
|
+
size:
|
|
1009
|
+
oneOf:
|
|
1010
|
+
- type: integer
|
|
1011
|
+
- type: array
|
|
1012
|
+
items:
|
|
1013
|
+
type: integer
|
|
1014
|
+
|
|
1015
|
+
required:
|
|
1016
|
+
type: boolean
|
|
1017
|
+
|
|
1018
|
+
default:
|
|
1019
|
+
type: [string, integer, float, boolean]
|
|
1020
|
+
|
|
1021
|
+
description:
|
|
1022
|
+
type: string
|
|
1023
|
+
|
|
1024
|
+
flags:
|
|
1025
|
+
type: array
|
|
1026
|
+
items:
|
|
1027
|
+
type: object
|
|
1028
|
+
$required:
|
|
1029
|
+
- name
|
|
1030
|
+
- cli
|
|
1031
|
+
properties:
|
|
1032
|
+
name:
|
|
1033
|
+
type: string
|
|
1034
|
+
|
|
1035
|
+
cli:
|
|
1036
|
+
type: string
|
|
1037
|
+
|
|
1038
|
+
cli_short:
|
|
1039
|
+
type: string
|
|
1040
|
+
|
|
1041
|
+
default:
|
|
1042
|
+
type: boolean
|
|
1043
|
+
|
|
1044
|
+
description:
|
|
1045
|
+
type: string
|
|
1046
|
+
|
|
1047
|
+
env_vars:
|
|
1048
|
+
type: array
|
|
1049
|
+
items:
|
|
1050
|
+
type: object
|
|
1051
|
+
$required:
|
|
1052
|
+
- name
|
|
1053
|
+
properties:
|
|
1054
|
+
name:
|
|
1055
|
+
type: string
|
|
1056
|
+
|
|
1057
|
+
value:
|
|
1058
|
+
type: string
|
|
1059
|
+
|
|
1060
|
+
from:
|
|
1061
|
+
type: string
|
|
1062
|
+
|
|
1063
|
+
default:
|
|
1064
|
+
type: string
|
|
1065
|
+
|
|
1066
|
+
platforms:
|
|
1067
|
+
type: array
|
|
1068
|
+
items:
|
|
1069
|
+
type: string
|
|
1070
|
+
|
|
1071
|
+
description:
|
|
1072
|
+
type: string
|
|
1073
|
+
|
|
1074
|
+
parse_output:
|
|
1075
|
+
type: object
|
|
1076
|
+
properties:
|
|
1077
|
+
type:
|
|
1078
|
+
type: string
|
|
1079
|
+
enum: [hash, array, string, integer]
|
|
1080
|
+
|
|
1081
|
+
pattern:
|
|
1082
|
+
type: string
|
|
1083
|
+
|
|
1084
|
+
format:
|
|
1085
|
+
type: string
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
**Validation with `json-schema` gem:**
|
|
1089
|
+
|
|
1090
|
+
```ruby
|
|
1091
|
+
# In registry Rakefile
|
|
1092
|
+
require 'json-schema'
|
|
1093
|
+
require 'yaml'
|
|
1094
|
+
|
|
1095
|
+
namespace :validate do
|
|
1096
|
+
desc "Validate all tool profiles against schema"
|
|
1097
|
+
task :tools do
|
|
1098
|
+
schema = JSON::Schema.new(YAML.load_file('schemas/tool-profile.yaml.schema'))
|
|
1099
|
+
|
|
1100
|
+
Dir.glob('tools/*/*.yaml').each do |file|
|
|
1101
|
+
puts "Validating #{file}..."
|
|
1102
|
+
profile = YAML.load_file(file)
|
|
1103
|
+
schema.validate(profile)
|
|
1104
|
+
puts " ✓ Valid"
|
|
1105
|
+
end
|
|
1106
|
+
rescue JSON::Schema::ValidationError => e
|
|
1107
|
+
puts " ✗ Validation failed: #{e.message}"
|
|
1108
|
+
exit 1
|
|
1109
|
+
end
|
|
1110
|
+
|
|
1111
|
+
desc "Validate schema files themselves"
|
|
1112
|
+
task :schemas do
|
|
1113
|
+
# Verify schemas are valid YAML Schema
|
|
1114
|
+
Dir.glob('schemas/*.yaml.schema').each do |file|
|
|
1115
|
+
puts "Validating schema #{file}..."
|
|
1116
|
+
schema = YAML.load_file(file)
|
|
1117
|
+
puts " ✓ Valid YAML Schema"
|
|
1118
|
+
end
|
|
1119
|
+
end
|
|
1120
|
+
|
|
1121
|
+
task all: [:schemas, :tools]
|
|
1122
|
+
end
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
### Usage Examples
|
|
1126
|
+
|
|
1127
|
+
```ruby
|
|
1128
|
+
# Load from registry
|
|
1129
|
+
Ukiryu::Registry.load_from("~/.ukiryu/register")
|
|
1130
|
+
|
|
1131
|
+
# Or load from gem
|
|
1132
|
+
Ukiryu::Registry.load_builtins
|
|
1133
|
+
|
|
1134
|
+
# Get tool
|
|
1135
|
+
inkscape = Ukiryu::Tool.get("inkscape")
|
|
1136
|
+
|
|
1137
|
+
# Detect platform, shell, version automatically
|
|
1138
|
+
# (or configure explicitly)
|
|
1139
|
+
inkscape.configure(
|
|
1140
|
+
platform: :macos,
|
|
1141
|
+
shell: :zsh
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
# Export command
|
|
1145
|
+
inkscape.export(
|
|
1146
|
+
inputs: ["diagram.svg"],
|
|
1147
|
+
output: "diagram.pdf",
|
|
1148
|
+
format: :pdf,
|
|
1149
|
+
plain: true,
|
|
1150
|
+
dpi: 300
|
|
1151
|
+
)
|
|
1152
|
+
|
|
1153
|
+
# Query command
|
|
1154
|
+
result = inkscape.query(
|
|
1155
|
+
input: "diagram.svg",
|
|
1156
|
+
width: true,
|
|
1157
|
+
height: true
|
|
1158
|
+
)
|
|
1159
|
+
# => { width: 1024, height: 768 }
|
|
1160
|
+
```
|
|
1161
|
+
|
|
1162
|
+
---
|
|
1163
|
+
|
|
1164
|
+
## Core DSL Design (For Custom Tools)
|
|
1165
|
+
|
|
1166
|
+
For tools not in the registry, users can still use Ruby DSL:
|
|
1167
|
+
|
|
1168
|
+
### Tool Declaration
|
|
1169
|
+
|
|
1170
|
+
```ruby
|
|
1171
|
+
class MyToolWrapper < Ukiryu::Wrapper
|
|
1172
|
+
tool "mytool" do
|
|
1173
|
+
# ---------- EXECUTABLE ----------
|
|
1174
|
+
|
|
1175
|
+
# Names to try (tried in order until one is found)
|
|
1176
|
+
names "mytool", "mt", "mytool-cli"
|
|
1177
|
+
|
|
1178
|
+
# Search paths (always includes ENV["PATH"])
|
|
1179
|
+
# Platform-specific additions:
|
|
1180
|
+
search_paths do
|
|
1181
|
+
# Windows-specific
|
|
1182
|
+
on_windows do
|
|
1183
|
+
"C:/Program Files/MyTool/*/mytool.exe"
|
|
1184
|
+
"#{ENV['PROGRAMFILES']}/MyTool/mytool.exe"
|
|
1185
|
+
end
|
|
1186
|
+
|
|
1187
|
+
# macOS app bundles
|
|
1188
|
+
on_macos do
|
|
1189
|
+
"/Applications/MyTool.app/Contents/MacOS/mytool"
|
|
1190
|
+
end
|
|
1191
|
+
|
|
1192
|
+
# Unix (no hardcoded paths - rely on PATH)
|
|
1193
|
+
# on_unix do
|
|
1194
|
+
# No hardcoded paths - rely on PATH only
|
|
1195
|
+
end
|
|
1196
|
+
end
|
|
1197
|
+
|
|
1198
|
+
# ---------- VERSION ----------
|
|
1199
|
+
|
|
1200
|
+
detect_version do
|
|
1201
|
+
run "--version"
|
|
1202
|
+
match /MyTool (\d+\.\d+\.\d+)/
|
|
1203
|
+
modern "2.0.0"
|
|
1204
|
+
end
|
|
1205
|
+
|
|
1206
|
+
# ---------- PROFILES ----------
|
|
1207
|
+
|
|
1208
|
+
# Each profile matches: platform + shell + version
|
|
1209
|
+
# All three must match for profile to be selected
|
|
1210
|
+
|
|
1211
|
+
# Modern (2.0+) on Bash/Zsh on Unix
|
|
1212
|
+
profile :modern_unix_bash,
|
|
1213
|
+
platform: [:macos, :linux],
|
|
1214
|
+
shell: [:bash, :zsh],
|
|
1215
|
+
version: ">= 2.0" do
|
|
1216
|
+
arguments do |params|
|
|
1217
|
+
[params.input, params.option_as_cli("--format="), params.flag_as_flag(:plain)]
|
|
1218
|
+
end
|
|
1219
|
+
end
|
|
1220
|
+
|
|
1221
|
+
# Modern on PowerShell on Windows
|
|
1222
|
+
profile :modern_windows_powershell,
|
|
1223
|
+
platform: :windows,
|
|
1224
|
+
shell: :powershell,
|
|
1225
|
+
version: ">= 2.0" do
|
|
1226
|
+
arguments do |params|
|
|
1227
|
+
[params.input, params.option_as_cli("/Format:"), params.flag_as_flag("/Plain")]
|
|
1228
|
+
end
|
|
1229
|
+
end
|
|
1230
|
+
|
|
1231
|
+
# Legacy on cmd on Windows
|
|
1232
|
+
profile :legacy_windows_cmd,
|
|
1233
|
+
platform: :windows,
|
|
1234
|
+
shell: :cmd,
|
|
1235
|
+
version: "< 2.0" do
|
|
1236
|
+
arguments do |params|
|
|
1237
|
+
[params.input, params.option_as_cli("/Format:"), params.flag_as_flag("/Plain")]
|
|
1238
|
+
end
|
|
1239
|
+
end
|
|
1240
|
+
|
|
1241
|
+
# ---------- GLOBAL DEFAULTS ----------
|
|
1242
|
+
|
|
1243
|
+
timeout 90
|
|
1244
|
+
terminate_with :TERM, after: 5, then: :KILL
|
|
1245
|
+
on_windows { terminate_with :KILL } # Windows only supports KILL reliably
|
|
1246
|
+
shell :bash # Default shell detection fallback
|
|
1247
|
+
end
|
|
1248
|
+
end
|
|
1249
|
+
```
|
|
1250
|
+
|
|
1251
|
+
### Type System
|
|
1252
|
+
|
|
1253
|
+
All parameter types with shell escaping behavior:
|
|
1254
|
+
|
|
1255
|
+
| Type | Ruby Type | Shell Escaping | Example |
|
|
1256
|
+
|------|-----------|----------------|---------|
|
|
1257
|
+
| `:file` | String path | Platform-specific | `'/path/file.pdf'` (Bash) |
|
|
1258
|
+
| `:string` | String | Fully escaped | `'Hello $USER'` (Bash, literal) |
|
|
1259
|
+
| `:integer` | Integer | No escaping (numeric) | `95` |
|
|
1260
|
+
| `:float` | Float | No escaping (numeric) | `95.5` |
|
|
1261
|
+
| `:symbol` | Symbol | No escaping (alnum) | `pdf` |
|
|
1262
|
+
| `:boolean` | Boolean | Flag or value | `--plain` added/not added |
|
|
1263
|
+
| `:uri` | String | Fully escaped | `'https://example.com'` |
|
|
1264
|
+
| `:datetime` | DateTime/String | Formatted then escaped | `'2025-01-21'` |
|
|
1265
|
+
| `:hash` | Hash | Recursively escaped/merged | `{:key => "value"}` |
|
|
1266
|
+
|
|
1267
|
+
### Parameter Definitions
|
|
1268
|
+
|
|
1269
|
+
```ruby
|
|
1270
|
+
command :convert do
|
|
1271
|
+
# POSITIONAL argument (order matters!)
|
|
1272
|
+
argument :input,
|
|
1273
|
+
type: :file,
|
|
1274
|
+
position: 1,
|
|
1275
|
+
required: true
|
|
1276
|
+
|
|
1277
|
+
# OPTION (named with value)
|
|
1278
|
+
option :format,
|
|
1279
|
+
type: :symbol,
|
|
1280
|
+
values: [:pdf, :eps, :svg],
|
|
1281
|
+
cli: "--format=",
|
|
1282
|
+
required: true
|
|
1283
|
+
|
|
1284
|
+
# FLAG (boolean, presence = true)
|
|
1285
|
+
flag :plain,
|
|
1286
|
+
cli: "--plain",
|
|
1287
|
+
default: false
|
|
1288
|
+
|
|
1289
|
+
# OUTPUT (where output goes)
|
|
1290
|
+
output :file,
|
|
1291
|
+
type: :file,
|
|
1292
|
+
via: :option, # Uses --export-filename= option
|
|
1293
|
+
cli: "--export-filename="
|
|
1294
|
+
end
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
### Type Validation Examples
|
|
1298
|
+
|
|
1299
|
+
| Type | Input | Validation | Shell Escaped As |
|
|
1300
|
+
|------|-------|------------|------------------|
|
|
1301
|
+
| `:file` | `"file.pdf"` | File exists? | `'file.pdf'` (Bash) |
|
|
1302
|
+
| `:string` | `"Hello World"` | Not empty | `'Hello World'` |
|
|
1303
|
+
| `:integer` | `95` | In 1..100 | `95` (no escape) |
|
|
1304
|
+
| `:symbol` | `:pdf` | In whitelist | `pdf` (no escape) |
|
|
1305
|
+
| `:boolean` | `true` | N/A | `--plain` added |
|
|
1306
|
+
| `:uri` | `https://example.com` | Valid URI | `'https://example.com'` |
|
|
1307
|
+
| `:datetime` | `DateTime.now` | Parseable | `'2025-01-21'` |
|
|
1308
|
+
|
|
1309
|
+
---
|
|
1310
|
+
|
|
1311
|
+
## Shell Detection (EXPLICIT)
|
|
1312
|
+
|
|
1313
|
+
### Detection Algorithm
|
|
1314
|
+
|
|
1315
|
+
```ruby
|
|
1316
|
+
module Ukiryu
|
|
1317
|
+
class Shell
|
|
1318
|
+
class << self
|
|
1319
|
+
def detect
|
|
1320
|
+
# Unix/macOS
|
|
1321
|
+
if unix?
|
|
1322
|
+
shell_from_env
|
|
1323
|
+
end
|
|
1324
|
+
|
|
1325
|
+
# Windows
|
|
1326
|
+
if windows?
|
|
1327
|
+
detect_windows_shell
|
|
1328
|
+
end
|
|
1329
|
+
|
|
1330
|
+
raise Ukiryu::UnknownShellError, <<~ERROR
|
|
1331
|
+
Unable to detect shell automatically. Please configure explicitly:
|
|
1332
|
+
|
|
1333
|
+
Ukiryu::Shell.configure do |config|
|
|
1334
|
+
config.shell = :bash # or :powershell, :cmd, :zsh
|
|
1335
|
+
end
|
|
1336
|
+
|
|
1337
|
+
Current environment:
|
|
1338
|
+
Platform: #{RbConfig::CONFIG['host_os']}
|
|
1339
|
+
SHELL: #{ENV['SHELL']}
|
|
1340
|
+
PSModulePath: #{ENV['PSModulePATH']}
|
|
1341
|
+
ERROR
|
|
1342
|
+
end
|
|
1343
|
+
|
|
1344
|
+
private
|
|
1345
|
+
|
|
1346
|
+
def unix?
|
|
1347
|
+
RbConfig::CONFIG['host_os'] !~ /mswin|mingw|windows/
|
|
1348
|
+
end
|
|
1349
|
+
|
|
1350
|
+
def windows?
|
|
1351
|
+
Gem.win_platform?
|
|
1352
|
+
end
|
|
1353
|
+
|
|
1354
|
+
def detect_windows_shell
|
|
1355
|
+
return :powershell if ENV['PSModulePath']
|
|
1356
|
+
return :bash if ENV['MSYSTEM'] || ENV['MINGW_PREFIX']
|
|
1357
|
+
return :bash if ENV['WSL_DISTRO']
|
|
1358
|
+
return :cmd # Default Windows shell
|
|
1359
|
+
end
|
|
1360
|
+
|
|
1361
|
+
def shell_from_env
|
|
1362
|
+
return :bash if ENV['SHELL'].end_with?('bash')
|
|
1363
|
+
return :zsh if ENV['SHELL'].end_with?('zsh')
|
|
1364
|
+
return :fish if ENV['SHELL'].end_with?('fish')
|
|
1365
|
+
return :sh if ENV['SHELL'].end_with?('sh')
|
|
1366
|
+
|
|
1367
|
+
# Unknown shell in ENV - try to detect
|
|
1368
|
+
shell_path = ENV['SHELL']
|
|
1369
|
+
if File.executable?(shell_path)
|
|
1370
|
+
name = File.basename(shell_path)
|
|
1371
|
+
return name.to_sym
|
|
1372
|
+
end
|
|
1373
|
+
|
|
1374
|
+
raise Ukiryu::UnknownShellError, "Unknown shell: #{ENV['SHELL']}"
|
|
1375
|
+
end
|
|
1376
|
+
end
|
|
1377
|
+
end
|
|
1378
|
+
end
|
|
1379
|
+
```
|
|
1380
|
+
|
|
1381
|
+
### Supported Shells
|
|
1382
|
+
|
|
1383
|
+
| Shell | Platform | Detection | Quote | Escape | Env Var |
|
|
1384
|
+
|-------|----------|-----------|------|--------|---------|
|
|
1385
|
+
| Bash | Unix/macOS/Linux | `$SHELL` ends with `bash` | `'str'` | `'\\''` | `$VAR` |
|
|
1386
|
+
| Zsh | Unix/macOS/Linux | `$SHELL` ends with `zsh` | `'str'` | `'\\''` | `$VAR` |
|
|
1387
|
+
| Fish | Unix/macOS/Linux | `$SHELL` ends with `fish` | `'str'` | `'\\''` | `$VAR` |
|
|
1388
|
+
| Sh | Unix/minimal | `$SHELL` ends with `sh` | `'str'` | `'\\''` | `$VAR` |
|
|
1389
|
+
| PowerShell | Windows | `ENV['PSModulePath']` exists | `'str'` | `` ` `` | `$ENV:NAME` |
|
|
1390
|
+
| Cmd | Windows | Default on Windows | `"` | `^` | `%VAR%` |
|
|
1391
|
+
|
|
1392
|
+
### Shell Configuration
|
|
1393
|
+
|
|
1394
|
+
```ruby
|
|
1395
|
+
# Global configuration
|
|
1396
|
+
Ukiryu::Shell.configure do |config|
|
|
1397
|
+
config.shell = :powershell
|
|
1398
|
+
end
|
|
1399
|
+
|
|
1400
|
+
# Per-tool configuration
|
|
1401
|
+
tool "mytool" do
|
|
1402
|
+
shell :bash # Force bash for this tool
|
|
1403
|
+
end
|
|
1404
|
+
|
|
1405
|
+
# Per-execution override
|
|
1406
|
+
result = execute(cmd, shell: :zsh)
|
|
1407
|
+
```
|
|
1408
|
+
|
|
1409
|
+
---
|
|
1410
|
+
|
|
1411
|
+
## Command Profiles (EXACT Matching)
|
|
1412
|
+
|
|
1413
|
+
### Profile Matching Rules
|
|
1414
|
+
|
|
1415
|
+
**Rule 1: EXACT match on Platform + Shell**
|
|
1416
|
+
- Must match both platform and shell exactly
|
|
1417
|
+
- `platform: :windows, shell: :powershell` ≠ `platform: :windows, shell: :bash`
|
|
1418
|
+
|
|
1419
|
+
**Rule 2: Version compatibility**
|
|
1420
|
+
- If tool is version 8.0 and profile exists for 7.0 → can use 7.0 profile
|
|
1421
|
+
- Assumes backward compatibility within major versions
|
|
1422
|
+
|
|
1423
|
+
**Rule 3: No partial matches**
|
|
1424
|
+
- If no exact profile matches → raise `Ukiryu::ProfileNotFoundError`
|
|
1425
|
+
- No "fallback to generic" - explicit or fail
|
|
1426
|
+
|
|
1427
|
+
### Profile Declaration
|
|
1428
|
+
|
|
1429
|
+
```ruby
|
|
1430
|
+
tool "inkscape" do
|
|
1431
|
+
|
|
1432
|
+
# ---------- EXACT PROFILES ----------
|
|
1433
|
+
|
|
1434
|
+
# macOS + Bash + Modern (1.0+)
|
|
1435
|
+
profile :macos_bash_modern,
|
|
1436
|
+
platform: :macos,
|
|
1437
|
+
shell: :bash,
|
|
1438
|
+
version: ">= 1.0" do
|
|
1439
|
+
# Define how to build command line from parameters
|
|
1440
|
+
arguments do |params|
|
|
1441
|
+
[
|
|
1442
|
+
params.input, # Positional arg 1
|
|
1443
|
+
params.option_as_cli("--export-type="), # Named option
|
|
1444
|
+
params.flag_as_flag(:plain) # Optional flag
|
|
1445
|
+
]
|
|
1446
|
+
end
|
|
1447
|
+
end
|
|
1448
|
+
|
|
1449
|
+
# Windows + PowerShell + Modern (1.0+)
|
|
1450
|
+
profile :windows_powershell_modern,
|
|
1451
|
+
platform: :windows,
|
|
1452
|
+
shell: :powershell,
|
|
1453
|
+
version: ">= 1.0" do
|
|
1454
|
+
arguments do |params|
|
|
1455
|
+
[
|
|
1456
|
+
params.input,
|
|
1457
|
+
params.option_as_cli("--export-type="),
|
|
1458
|
+
params.flag_as_flag(:plain)
|
|
1459
|
+
]
|
|
1460
|
+
end
|
|
1461
|
+
|
|
1462
|
+
# Windows + cmd + Legacy (< 1.0)
|
|
1463
|
+
profile :windows_cmd_legacy,
|
|
1464
|
+
platform: :windows,
|
|
1465
|
+
shell: :cmd,
|
|
1466
|
+
version: "< 1.0" do
|
|
1467
|
+
arguments do |params|
|
|
1468
|
+
[
|
|
1469
|
+
params.input,
|
|
1470
|
+
params.option_as_cli("--export-type="),
|
|
1471
|
+
params.flag_as_flag(:plain)
|
|
1472
|
+
]
|
|
1473
|
+
end
|
|
1474
|
+
|
|
1475
|
+
# Windows + Git Bash + Modern
|
|
1476
|
+
profile :windows_bash_modern,
|
|
1477
|
+
platform: :windows,
|
|
1478
|
+
shell: :bash,
|
|
1479
|
+
version: ">= 1.0" do
|
|
1480
|
+
arguments do |params|
|
|
1481
|
+
[
|
|
1482
|
+
params.input,
|
|
1483
|
+
params.option_as_cli("--export-type="),
|
|
1484
|
+
params.flag_as_flag(:plain)
|
|
1485
|
+
]
|
|
1486
|
+
end
|
|
1487
|
+
end
|
|
1488
|
+
```
|
|
1489
|
+
|
|
1490
|
+
### Profile Selection Algorithm
|
|
1491
|
+
|
|
1492
|
+
```ruby
|
|
1493
|
+
module Ukiryu
|
|
1494
|
+
class ProfileSelector
|
|
1495
|
+
class << self
|
|
1496
|
+
def select(tool_name, platform:, shell:, version:)
|
|
1497
|
+
profiles = profiles_for(tool_name)
|
|
1498
|
+
|
|
1499
|
+
# Find EXACT matches
|
|
1500
|
+
matches = profiles.select do |profile|
|
|
1501
|
+
profile.platform == platform &&
|
|
1502
|
+
profile.shell == shell &&
|
|
1503
|
+
profile.satisfies_version?(version)
|
|
1504
|
+
end
|
|
1505
|
+
|
|
1506
|
+
return matches.first if matches.any?
|
|
1507
|
+
|
|
1508
|
+
# No exact match: try compatible versions
|
|
1509
|
+
compatible = profiles.select do |profile|
|
|
1510
|
+
profile.platform == platform &&
|
|
1511
|
+
profile.shell == shell &&
|
|
1512
|
+
profile.compatible_with?(version)
|
|
1513
|
+
end
|
|
1514
|
+
|
|
1515
|
+
return compatible.first if compatible.any?
|
|
1516
|
+
|
|
1517
|
+
# Nothing matches: raise error
|
|
1518
|
+
available = profiles.map(&:description).join(", ")
|
|
1519
|
+
raise ProfileNotFoundError, <<~ERROR
|
|
1520
|
+
No matching profile found for:
|
|
1521
|
+
Tool: #{tool_name}
|
|
1522
|
+
Platform: #{platform}
|
|
1523
|
+
Shell: #{shell}
|
|
1524
|
+
Version: #{version}
|
|
1525
|
+
|
|
1526
|
+
Available profiles:
|
|
1527
|
+
#{available}
|
|
1528
|
+
|
|
1529
|
+
Please configure Ukiryu with the correct profile.
|
|
1530
|
+
ERROR
|
|
1531
|
+
end
|
|
1532
|
+
end
|
|
1533
|
+
end
|
|
1534
|
+
end
|
|
1535
|
+
```
|
|
1536
|
+
|
|
1537
|
+
---
|
|
1538
|
+
|
|
1539
|
+
## Complete DSL Specification
|
|
1540
|
+
|
|
1541
|
+
### Tool Declaration
|
|
1542
|
+
|
|
1543
|
+
```ruby
|
|
1544
|
+
class MyToolWrapper < Ukiryu::Wrapper
|
|
1545
|
+
tool "mytool" do
|
|
1546
|
+
# ---------- EXECUTABLE ----------
|
|
1547
|
+
|
|
1548
|
+
# Names to try (tried in order until found)
|
|
1549
|
+
names "mytool", "mt", "mytool-cli"
|
|
1550
|
+
|
|
1551
|
+
# Search paths (always includes ENV["PATH"])
|
|
1552
|
+
# Add platform-specific paths only if not in PATH
|
|
1553
|
+
search_paths do
|
|
1554
|
+
on_windows { "C:/Program Files/MyTool/*/mytool.exe" }
|
|
1555
|
+
on_macos { "/Applications/MyTool.app/Contents/MacOS/mytool" }
|
|
1556
|
+
# Unix: no hardcoded paths (rely on PATH)
|
|
1557
|
+
end
|
|
1558
|
+
|
|
1559
|
+
# ---------- VERSION ----------
|
|
1560
|
+
|
|
1561
|
+
detect_version do
|
|
1562
|
+
run "--version"
|
|
1563
|
+
match /MyTool (\d+\.\d+)/
|
|
1564
|
+
modern "2.0.0"
|
|
1565
|
+
end
|
|
1566
|
+
|
|
1567
|
+
# ---------- PROFILES ----------
|
|
1568
|
+
|
|
1569
|
+
# Define multiple profiles for different combos
|
|
1570
|
+
profile :modern_bash,
|
|
1571
|
+
platform: [:macos, :linux],
|
|
1572
|
+
shell: [:bash, :zsh],
|
|
1573
|
+
version: ">= 2.0" do
|
|
1574
|
+
option_style :double_dash
|
|
1575
|
+
separator "="
|
|
1576
|
+
end
|
|
1577
|
+
|
|
1578
|
+
profile :legacy_cmd,
|
|
1579
|
+
platform: :windows,
|
|
1580
|
+
shell: :cmd,
|
|
1581
|
+
version: "< 2.0" do
|
|
1582
|
+
option_style :slash
|
|
1583
|
+
separator ":"
|
|
1584
|
+
end
|
|
1585
|
+
|
|
1586
|
+
# ---------- GLOBAL DEFAULTS ----------
|
|
1587
|
+
|
|
1588
|
+
timeout 90
|
|
1589
|
+
terminate_with :TERM, after: 5, then: :KILL
|
|
1590
|
+
shell :bash # Default detection fallback
|
|
1591
|
+
end
|
|
1592
|
+
end
|
|
1593
|
+
```
|
|
1594
|
+
|
|
1595
|
+
---
|
|
1596
|
+
|
|
1597
|
+
## Option Format Variations
|
|
1598
|
+
|
|
1599
|
+
CLI tools use many different option formats. Ukiryu supports all common patterns:
|
|
1600
|
+
|
|
1601
|
+
### Format Types
|
|
1602
|
+
|
|
1603
|
+
| Format | Example | CLI Syntax | DSL Specification |
|
|
1604
|
+
|--------|---------|------------|------------------|
|
|
1605
|
+
| Double-dash equals | `--format=pdf` | `--flag=value` | `cli: "--format="` |
|
|
1606
|
+
| Double-dash space | `--format pdf` | `--flag value` | `cli: "--format", separator: " "` |
|
|
1607
|
+
| Single-dash equals | `-f=pdf` | `-f=value` | `cli: "-f="` |
|
|
1608
|
+
| Single-dash space | `-f pdf` | `-f value` | `cli: "-f", separator: " "` |
|
|
1609
|
+
| Windows slash | `/format pdf` | `/flag value` | `cli: "/format", separator: " "` |
|
|
1610
|
+
| Single-letter flag | `-v` | Just the flag | `flag :verbose, cli: "-v"` |
|
|
1611
|
+
| Combined flags | `-v -a -q` → `-vaq` | `-vaq` | Automatic for single-letter flags |
|
|
1612
|
+
| Value in flag | `-r300` | Value embedded | `cli: "-r", value_position: :embedded` |
|
|
1613
|
+
|
|
1614
|
+
### Option Format DSL
|
|
1615
|
+
|
|
1616
|
+
```ruby
|
|
1617
|
+
command :export do
|
|
1618
|
+
# Double-dash with equals (GNU style)
|
|
1619
|
+
option :format,
|
|
1620
|
+
type: :symbol,
|
|
1621
|
+
values: [:pdf, :eps, :svg],
|
|
1622
|
+
cli: "--format=",
|
|
1623
|
+
format: :double_dash_equals # --format=pdf
|
|
1624
|
+
|
|
1625
|
+
# Double-dash with space (POSIX long)
|
|
1626
|
+
option :dpi,
|
|
1627
|
+
type: :integer,
|
|
1628
|
+
cli: "--dpi",
|
|
1629
|
+
format: :double_dash_space, # --dpi 96
|
|
1630
|
+
separator: " "
|
|
1631
|
+
|
|
1632
|
+
# Single-dash short option
|
|
1633
|
+
option :quality,
|
|
1634
|
+
type: :integer,
|
|
1635
|
+
cli: "-q",
|
|
1636
|
+
format: :single_dash_equals, # -q=95
|
|
1637
|
+
separator: "="
|
|
1638
|
+
|
|
1639
|
+
# Windows-style slash
|
|
1640
|
+
option :output,
|
|
1641
|
+
type: :file,
|
|
1642
|
+
cli: "/Output",
|
|
1643
|
+
format: :slash_space, # /Output file.pdf
|
|
1644
|
+
separator: " "
|
|
1645
|
+
end
|
|
1646
|
+
```
|
|
1647
|
+
|
|
1648
|
+
### Profile-Specific Option Formats
|
|
1649
|
+
|
|
1650
|
+
Different platforms/shells use different formats:
|
|
1651
|
+
|
|
1652
|
+
```ruby
|
|
1653
|
+
tool "mytool" do
|
|
1654
|
+
# Unix/modern: double-dash equals
|
|
1655
|
+
profile :modern_unix,
|
|
1656
|
+
platform: [:macos, :linux],
|
|
1657
|
+
shell: [:bash, :zsh] do
|
|
1658
|
+
option_style :double_dash_equals
|
|
1659
|
+
end
|
|
1660
|
+
|
|
1661
|
+
# Windows PowerShell: slash with space
|
|
1662
|
+
profile :windows_powershell,
|
|
1663
|
+
platform: :windows,
|
|
1664
|
+
shell: :powershell do
|
|
1665
|
+
option_style :slash_space
|
|
1666
|
+
end
|
|
1667
|
+
|
|
1668
|
+
# Windows cmd: slash with colon
|
|
1669
|
+
profile :windows_cmd,
|
|
1670
|
+
platform: :windows,
|
|
1671
|
+
shell: :cmd do
|
|
1672
|
+
option_style :slash_colon
|
|
1673
|
+
end
|
|
1674
|
+
end
|
|
1675
|
+
```
|
|
1676
|
+
|
|
1677
|
+
### Real-World Inkscape Example
|
|
1678
|
+
|
|
1679
|
+
```ruby
|
|
1680
|
+
class Inkscape < Ukiryu::Wrapper
|
|
1681
|
+
tool "inkscape" do
|
|
1682
|
+
names "inkscape", "inkscapecom"
|
|
1683
|
+
detect_version { run "--version"; match /Inkscape (\d+\.\d+)/ }
|
|
1684
|
+
end
|
|
1685
|
+
|
|
1686
|
+
command :export do
|
|
1687
|
+
# Inkscape has MANY option formats!
|
|
1688
|
+
|
|
1689
|
+
# Double-dash equals: --export-filename=out.pdf
|
|
1690
|
+
option :output,
|
|
1691
|
+
type: :file,
|
|
1692
|
+
cli: "--export-filename=",
|
|
1693
|
+
format: :double_dash_equals
|
|
1694
|
+
|
|
1695
|
+
# Double-dash space: --export-type pdf
|
|
1696
|
+
option :format,
|
|
1697
|
+
type: :symbol,
|
|
1698
|
+
values: [:svg, :png, :ps, :eps, :pdf, :emf, :wmf, :xaml],
|
|
1699
|
+
cli: "--export-type",
|
|
1700
|
+
format: :double_dash_space,
|
|
1701
|
+
separator: " "
|
|
1702
|
+
|
|
1703
|
+
# Single-letter with equals: -o=out.pdf (alias for --export-filename)
|
|
1704
|
+
option :output_short,
|
|
1705
|
+
type: :file,
|
|
1706
|
+
cli: "-o=",
|
|
1707
|
+
format: :single_dash_equals
|
|
1708
|
+
|
|
1709
|
+
# Single-letter space: -d 96
|
|
1710
|
+
option :dpi,
|
|
1711
|
+
type: :integer,
|
|
1712
|
+
cli: "-d",
|
|
1713
|
+
format: :single_dash_space,
|
|
1714
|
+
separator: " "
|
|
1715
|
+
|
|
1716
|
+
# Flag: --export-plain-svg
|
|
1717
|
+
flag :plain,
|
|
1718
|
+
cli: "--export-plain-svg"
|
|
1719
|
+
|
|
1720
|
+
# Single-letter flag: -l (alias for --export-plain-svg)
|
|
1721
|
+
flag :plain_short,
|
|
1722
|
+
cli: "-l"
|
|
1723
|
+
|
|
1724
|
+
# Variadic input files: inkscape [options] file1 [file2 ...]
|
|
1725
|
+
argument :inputs,
|
|
1726
|
+
type: :file,
|
|
1727
|
+
position: :last,
|
|
1728
|
+
variadic: true,
|
|
1729
|
+
min: 1
|
|
1730
|
+
end
|
|
1731
|
+
end
|
|
1732
|
+
```
|
|
1733
|
+
|
|
1734
|
+
**Usage examples:**
|
|
1735
|
+
|
|
1736
|
+
```ruby
|
|
1737
|
+
# Modern double-dash equals
|
|
1738
|
+
Inkscape.export(
|
|
1739
|
+
inputs: ["diagram.svg"],
|
|
1740
|
+
output: "diagram.pdf",
|
|
1741
|
+
format: :pdf,
|
|
1742
|
+
plain: true
|
|
1743
|
+
)
|
|
1744
|
+
# CLI: inkscape --export-filename=diagram.pdf --export-type pdf --export-plain-svg diagram.svg
|
|
1745
|
+
|
|
1746
|
+
# Using short options
|
|
1747
|
+
Inkscape.export(
|
|
1748
|
+
inputs: ["diagram.svg"],
|
|
1749
|
+
output_short: "diagram.png",
|
|
1750
|
+
dpi: 300
|
|
1751
|
+
)
|
|
1752
|
+
# CLI: inkscape -o=diagram.png -d 300 diagram.svg
|
|
1753
|
+
|
|
1754
|
+
# Multiple input files
|
|
1755
|
+
Inkscape.export(
|
|
1756
|
+
inputs: ["a.svg", "b.svg", "c.svg"],
|
|
1757
|
+
format: :png
|
|
1758
|
+
)
|
|
1759
|
+
# CLI: inkscape --export-type png a.svg b.svg c.svg
|
|
1760
|
+
# Results in: a.png, b.png, c.png (same directory, same base name)
|
|
1761
|
+
```
|
|
1762
|
+
|
|
1763
|
+
---
|
|
1764
|
+
|
|
1765
|
+
## Value Separators & Special Values
|
|
1766
|
+
|
|
1767
|
+
Many tools accept multiple values in a single option using separators:
|
|
1768
|
+
|
|
1769
|
+
### Separator Types
|
|
1770
|
+
|
|
1771
|
+
| Separator | Example | DSL | Use Case |
|
|
1772
|
+
|-----------|---------|-----|----------|
|
|
1773
|
+
| Comma | `--types=svg,png,pdf` | `separator: ","` | File types, extensions |
|
|
1774
|
+
| Semicolon | `--ids=obj1;obj2;obj3` | `separator: ";"` | Object IDs, lists |
|
|
1775
|
+
| Colon | `-r300x600` | `separator: "x"` | Dimensions (width×height) |
|
|
1776
|
+
| Pipe | `--files=a\|b\|c` | `separator: "\|"` | Alternative paths |
|
|
1777
|
+
| Space | `--search path1 path2` | `separator: " "` | Multiple paths |
|
|
1778
|
+
| Plus | `--pages=1+3+5` | `separator: "+"` | Page numbers |
|
|
1779
|
+
|
|
1780
|
+
### Value Separator DSL
|
|
1781
|
+
|
|
1782
|
+
```ruby
|
|
1783
|
+
command :process do
|
|
1784
|
+
# Comma-separated values
|
|
1785
|
+
option :formats,
|
|
1786
|
+
type: :array,
|
|
1787
|
+
of: :symbol,
|
|
1788
|
+
values: [:svg, :png, :pdf, :eps],
|
|
1789
|
+
cli: "--export-type=",
|
|
1790
|
+
separator: "," # --export-type=svg,png,pdf
|
|
1791
|
+
|
|
1792
|
+
# Semicolon-separated (Inkscape object IDs)
|
|
1793
|
+
option :objects,
|
|
1794
|
+
type: :array,
|
|
1795
|
+
of: :string,
|
|
1796
|
+
cli: "--export-id=",
|
|
1797
|
+
separator: ";" # --export-id=obj1;obj2;obj3
|
|
1798
|
+
|
|
1799
|
+
# Colon-separated (Ghostscript resolution)
|
|
1800
|
+
option :resolution,
|
|
1801
|
+
type: :array,
|
|
1802
|
+
of: :integer,
|
|
1803
|
+
size: 2, # Exactly 2 values
|
|
1804
|
+
cli: "-r",
|
|
1805
|
+
separator: "x", # -r300x600
|
|
1806
|
+
format: :single_dash_space
|
|
1807
|
+
|
|
1808
|
+
# Space-separated paths (Ghostscript -I)
|
|
1809
|
+
option :lib_paths,
|
|
1810
|
+
type: :array,
|
|
1811
|
+
of: :file,
|
|
1812
|
+
cli: "-I",
|
|
1813
|
+
separator: " ", # -I path1 path2 path3
|
|
1814
|
+
format: :single_dash_space
|
|
1815
|
+
end
|
|
1816
|
+
```
|
|
1817
|
+
|
|
1818
|
+
### Real-World Ghostscript Example
|
|
1819
|
+
|
|
1820
|
+
```ruby
|
|
1821
|
+
class Ghostscript < Ukiryu::Wrapper
|
|
1822
|
+
tool "gs" do
|
|
1823
|
+
names "gs", "gswin64c", "gswin32c", "gsos2"
|
|
1824
|
+
end
|
|
1825
|
+
|
|
1826
|
+
command :convert do
|
|
1827
|
+
# -sDEVICE=pdfwrite (string parameter, dash-s)
|
|
1828
|
+
option :device,
|
|
1829
|
+
type: :symbol,
|
|
1830
|
+
values: [:pdfwrite, :eps2write, :png16m, :jpeg],
|
|
1831
|
+
cli: "-sDEVICE=",
|
|
1832
|
+
format: :single_dash_equals
|
|
1833
|
+
|
|
1834
|
+
# -r300 or -r300x600 (resolution, dash-r)
|
|
1835
|
+
option :resolution,
|
|
1836
|
+
type: :array,
|
|
1837
|
+
of: :integer,
|
|
1838
|
+
size: [1, 2], # 1 or 2 values
|
|
1839
|
+
cli: "-r",
|
|
1840
|
+
format: :single_dash_space,
|
|
1841
|
+
separator: "x",
|
|
1842
|
+
value_position: :embedded # -r300 (no space)
|
|
1843
|
+
|
|
1844
|
+
# -sOutputFile=out.pdf (output, dash-s string)
|
|
1845
|
+
option :output,
|
|
1846
|
+
type: :file,
|
|
1847
|
+
cli: "-sOutputFile=",
|
|
1848
|
+
format: :single_dash_equals
|
|
1849
|
+
|
|
1850
|
+
# -dSAFER (define name, dash-d)
|
|
1851
|
+
flag :safer,
|
|
1852
|
+
cli: "-dSAFER",
|
|
1853
|
+
format: :single_dash
|
|
1854
|
+
|
|
1855
|
+
# -q (quiet, single letter flag)
|
|
1856
|
+
flag :quiet,
|
|
1857
|
+
cli: "-q"
|
|
1858
|
+
|
|
1859
|
+
# -I path1 path2 path3 (search paths, variadic option)
|
|
1860
|
+
option :lib_paths,
|
|
1861
|
+
type: :array,
|
|
1862
|
+
of: :file,
|
|
1863
|
+
cli: "-I",
|
|
1864
|
+
format: :single_dash_space,
|
|
1865
|
+
separator: " ",
|
|
1866
|
+
repeatable: true # Can use -I multiple times
|
|
1867
|
+
|
|
1868
|
+
# Input file(s) at end
|
|
1869
|
+
argument :inputs,
|
|
1870
|
+
type: :file,
|
|
1871
|
+
position: :last,
|
|
1872
|
+
variadic: true,
|
|
1873
|
+
min: 1
|
|
1874
|
+
end
|
|
1875
|
+
end
|
|
1876
|
+
```
|
|
1877
|
+
|
|
1878
|
+
**Usage examples:**
|
|
1879
|
+
|
|
1880
|
+
```ruby
|
|
1881
|
+
# Simple PDF conversion
|
|
1882
|
+
Ghostscript.convert(
|
|
1883
|
+
inputs: ["input.ps"],
|
|
1884
|
+
output: "output.pdf",
|
|
1885
|
+
device: :pdfwrite,
|
|
1886
|
+
safer: true,
|
|
1887
|
+
quiet: true
|
|
1888
|
+
)
|
|
1889
|
+
# CLI: gs -sDEVICE=pdfwrite -sOutputFile=output.pdf -dSAFER -q input.ps
|
|
1890
|
+
|
|
1891
|
+
# With resolution (single value)
|
|
1892
|
+
Ghostscript.convert(
|
|
1893
|
+
inputs: ["input.pdf"],
|
|
1894
|
+
output: "output.png",
|
|
1895
|
+
device: :png16m,
|
|
1896
|
+
resolution: [300]
|
|
1897
|
+
)
|
|
1898
|
+
# CLI: gs -sDEVICE=png16m -sOutputFile=output.png -r300 input.pdf
|
|
1899
|
+
|
|
1900
|
+
# With resolution (two values: X×Y)
|
|
1901
|
+
Ghostscript.convert(
|
|
1902
|
+
inputs: ["input.pdf"],
|
|
1903
|
+
output: "output.png",
|
|
1904
|
+
device: :png16m,
|
|
1905
|
+
resolution: [300, 600]
|
|
1906
|
+
)
|
|
1907
|
+
# CLI: gs -sDEVICE=png16m -sOutputFile=output.png -r300x600 input.pdf
|
|
1908
|
+
|
|
1909
|
+
# With library paths
|
|
1910
|
+
Ghostscript.convert(
|
|
1911
|
+
inputs: ["input.ps"],
|
|
1912
|
+
output: "output.pdf",
|
|
1913
|
+
lib_paths: ["/usr/local/share/ghostscript", "/custom/path"]
|
|
1914
|
+
)
|
|
1915
|
+
# CLI: gs -I /usr/local/share/ghostscript /custom/path -sDEVICE=pdfwrite ...
|
|
1916
|
+
```
|
|
1917
|
+
|
|
1918
|
+
### Special Values
|
|
1919
|
+
|
|
1920
|
+
Some tools use special values with meaning:
|
|
1921
|
+
|
|
1922
|
+
```ruby
|
|
1923
|
+
command :convert do
|
|
1924
|
+
# Ghostscript: -sOutputFile=- (stdout)
|
|
1925
|
+
option :output,
|
|
1926
|
+
type: :file,
|
|
1927
|
+
cli: "-sOutputFile=",
|
|
1928
|
+
special_values: {
|
|
1929
|
+
stdout: "-", # -sOutputFile=-
|
|
1930
|
+
pipe: "%pipe%lpr", # -sOutputFile=%pipe%lpr
|
|
1931
|
+
template: "%d" # -sOutputFile=file%d.pdf
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
# Inkscape: --export-type=TYPE[,TYPE]* (multiple types)
|
|
1935
|
+
option :export_types,
|
|
1936
|
+
type: :array,
|
|
1937
|
+
of: :symbol,
|
|
1938
|
+
values: [:svg, :png, :ps, :eps, :pdf],
|
|
1939
|
+
cli: "--export-type=",
|
|
1940
|
+
separator: ","
|
|
1941
|
+
end
|
|
1942
|
+
```
|
|
1943
|
+
|
|
1944
|
+
---
|
|
1945
|
+
|
|
1946
|
+
## Environment Variables
|
|
1947
|
+
|
|
1948
|
+
Tools often require custom environment variables:
|
|
1949
|
+
|
|
1950
|
+
### Environment Variable DSL
|
|
1951
|
+
|
|
1952
|
+
```ruby
|
|
1953
|
+
command :run do
|
|
1954
|
+
# Set environment variables for this command
|
|
1955
|
+
env_vars do
|
|
1956
|
+
set "MY_VAR", from: :option # From user-provided option
|
|
1957
|
+
set "PATH", append: "/usr/local/bin" # Append to existing
|
|
1958
|
+
set "DISPLAY", value: "" # Set explicitly (headless mode)
|
|
1959
|
+
set "MAGICK_CONFIGURE_PATH", from: :option, default: "/etc/ImageMagick"
|
|
1960
|
+
end
|
|
1961
|
+
|
|
1962
|
+
option :my_var,
|
|
1963
|
+
type: :string,
|
|
1964
|
+
desc: "Value for MY_VAR environment variable"
|
|
1965
|
+
|
|
1966
|
+
option :config_path,
|
|
1967
|
+
type: :file,
|
|
1968
|
+
desc: "Path for MAGICK_CONFIGURE_PATH"
|
|
1969
|
+
end
|
|
1970
|
+
```
|
|
1971
|
+
|
|
1972
|
+
### Real-World Examples
|
|
1973
|
+
|
|
1974
|
+
**Inkscape headless operation:**
|
|
1975
|
+
|
|
1976
|
+
```ruby
|
|
1977
|
+
class Inkscape < Ukiryu::Wrapper
|
|
1978
|
+
command :export do
|
|
1979
|
+
# Disable display on Unix/macOS for headless operation
|
|
1980
|
+
env_vars do
|
|
1981
|
+
set "DISPLAY", value: "", on: [:macos, :linux]
|
|
1982
|
+
# Windows: no DISPLAY variable needed
|
|
1983
|
+
end
|
|
1984
|
+
end
|
|
1985
|
+
end
|
|
1986
|
+
```
|
|
1987
|
+
|
|
1988
|
+
**ImageMagick with custom config:**
|
|
1989
|
+
|
|
1990
|
+
```ruby
|
|
1991
|
+
class ImageMagick < Ukiryu::Wrapper
|
|
1992
|
+
command :convert do
|
|
1993
|
+
env_vars do
|
|
1994
|
+
set "MAGICK_CONFIGURE_PATH", from: :config_dir
|
|
1995
|
+
set "MAGICK_HOME", from: :install_dir
|
|
1996
|
+
end
|
|
1997
|
+
|
|
1998
|
+
option :config_dir,
|
|
1999
|
+
type: :file,
|
|
2000
|
+
desc: "Custom configuration directory"
|
|
2001
|
+
|
|
2002
|
+
option :install_dir,
|
|
2003
|
+
type: :file,
|
|
2004
|
+
desc: "ImageMagick installation directory"
|
|
2005
|
+
end
|
|
2006
|
+
end
|
|
2007
|
+
```
|
|
2008
|
+
|
|
2009
|
+
**Ghostscript with library path:**
|
|
2010
|
+
|
|
2011
|
+
```ruby
|
|
2012
|
+
class Ghostscript < Ukiryu::Wrapper
|
|
2013
|
+
command :convert do
|
|
2014
|
+
env_vars do
|
|
2015
|
+
# GS_LIB is used to find initialization files and fonts
|
|
2016
|
+
set "GS_LIB", from: :lib_path
|
|
2017
|
+
end
|
|
2018
|
+
|
|
2019
|
+
option :lib_path,
|
|
2020
|
+
type: :file,
|
|
2021
|
+
desc: "Ghostscript library path"
|
|
2022
|
+
end
|
|
2023
|
+
end
|
|
2024
|
+
```
|
|
2025
|
+
|
|
2026
|
+
### Shell-Specific Environment Variable Syntax
|
|
2027
|
+
|
|
2028
|
+
| Shell | Syntax | Example |
|
|
2029
|
+
|-------|--------|---------|
|
|
2030
|
+
| Bash/Zsh | `VAR=value` | `DISPLAY="" ./command` |
|
|
2031
|
+
| PowerShell | `$ENV:VAR = "value"` | `$ENV:DISPLAY = ""; ./command` |
|
|
2032
|
+
| Cmd | `set VAR=value` | `set DISPLAY= && command` |
|
|
2033
|
+
|
|
2034
|
+
Ukiryu handles the correct syntax for each shell automatically.
|
|
2035
|
+
|
|
2036
|
+
---
|
|
2037
|
+
|
|
2038
|
+
## Subcommands (Git-Style)
|
|
2039
|
+
|
|
2040
|
+
Many tools use subcommands with their own options:
|
|
2041
|
+
|
|
2042
|
+
### Subcommand DSL
|
|
2043
|
+
|
|
2044
|
+
```ruby
|
|
2045
|
+
class Git < Ukiryu::Wrapper
|
|
2046
|
+
tool "git" do
|
|
2047
|
+
names "git"
|
|
2048
|
+
end
|
|
2049
|
+
|
|
2050
|
+
# Subcommand: git add
|
|
2051
|
+
subcommand :add, desc: "Add files to staging" do
|
|
2052
|
+
argument :files,
|
|
2053
|
+
type: :file,
|
|
2054
|
+
variadic: true,
|
|
2055
|
+
min: 0 # Can add nothing (stage current changes)
|
|
2056
|
+
|
|
2057
|
+
flag :all, cli: "--all"
|
|
2058
|
+
flag :update, cli: "--update"
|
|
2059
|
+
flag :verbose, cli: "-v"
|
|
2060
|
+
end
|
|
2061
|
+
|
|
2062
|
+
# Subcommand: git commit
|
|
2063
|
+
subcommand :commit, desc: "Commit changes" do
|
|
2064
|
+
flag :all, cli: "--all", cli_short: "-a"
|
|
2065
|
+
flag :amend, cli: "--amend"
|
|
2066
|
+
flag :verbose, cli: "--verbose", cli_short: "-v"
|
|
2067
|
+
|
|
2068
|
+
option :message,
|
|
2069
|
+
type: :string,
|
|
2070
|
+
cli: "-m",
|
|
2071
|
+
format: :single_dash_space,
|
|
2072
|
+
separator: " "
|
|
2073
|
+
|
|
2074
|
+
flag :no_edit, cli: "--no-edit"
|
|
2075
|
+
end
|
|
2076
|
+
|
|
2077
|
+
# Subcommand: git push
|
|
2078
|
+
subcommand :push, desc: "Push to remote" do
|
|
2079
|
+
argument :remote,
|
|
2080
|
+
type: :string,
|
|
2081
|
+
required: false,
|
|
2082
|
+
default: "origin"
|
|
2083
|
+
|
|
2084
|
+
argument :branch,
|
|
2085
|
+
type: :string,
|
|
2086
|
+
required: false
|
|
2087
|
+
|
|
2088
|
+
flag :force, cli: "--force", cli_short: "-f"
|
|
2089
|
+
flag :set_upstream, cli: "--set-upstream", cli_short: "-u"
|
|
2090
|
+
flag :verbose, cli: "--verbose", cli_short: "-v"
|
|
2091
|
+
end
|
|
2092
|
+
end
|
|
2093
|
+
```
|
|
2094
|
+
|
|
2095
|
+
### Subcommand Usage
|
|
2096
|
+
|
|
2097
|
+
```ruby
|
|
2098
|
+
# git add file1.rb file2.rb
|
|
2099
|
+
Git.add(files: ["file1.rb", "file2.rb"], all: false)
|
|
2100
|
+
# CLI: git add file1.rb file2.rb
|
|
2101
|
+
|
|
2102
|
+
# git add -A
|
|
2103
|
+
Git.add(files: [], all: true)
|
|
2104
|
+
# CLI: git add --all
|
|
2105
|
+
|
|
2106
|
+
# git commit -m "Fix bug"
|
|
2107
|
+
Git.commit(message: "Fix bug")
|
|
2108
|
+
# CLI: git commit -m "Fix bug"
|
|
2109
|
+
|
|
2110
|
+
# git commit -am "Update"
|
|
2111
|
+
Git.commit(all: true, message: "Update")
|
|
2112
|
+
# CLI: git commit -a -m "Update"
|
|
2113
|
+
|
|
2114
|
+
# git push origin main
|
|
2115
|
+
Git.push(remote: "origin", branch: "main")
|
|
2116
|
+
# CLI: git push origin main
|
|
2117
|
+
|
|
2118
|
+
# git push (uses defaults)
|
|
2119
|
+
Git.push
|
|
2120
|
+
# CLI: git push
|
|
2121
|
+
```
|
|
2122
|
+
|
|
2123
|
+
### Subcommand with Profiles
|
|
2124
|
+
|
|
2125
|
+
Different subcommands may have different profiles:
|
|
2126
|
+
|
|
2127
|
+
```ruby
|
|
2128
|
+
tool "docker" do
|
|
2129
|
+
# docker build
|
|
2130
|
+
subcommand :build do
|
|
2131
|
+
profile :modern,
|
|
2132
|
+
platform: :any,
|
|
2133
|
+
shell: :any,
|
|
2134
|
+
version: ">= 20.0" do
|
|
2135
|
+
# Docker 20+ uses --build-arg
|
|
2136
|
+
option :build_args,
|
|
2137
|
+
type: :hash,
|
|
2138
|
+
cli: "--build-arg="
|
|
2139
|
+
end
|
|
2140
|
+
|
|
2141
|
+
profile :legacy,
|
|
2142
|
+
platform: :any,
|
|
2143
|
+
shell: :any,
|
|
2144
|
+
version: "< 20.0" do
|
|
2145
|
+
# Docker < 20 used different syntax
|
|
2146
|
+
option :build_args,
|
|
2147
|
+
type: :hash,
|
|
2148
|
+
cli: "--build-arg"
|
|
2149
|
+
end
|
|
2150
|
+
end
|
|
2151
|
+
end
|
|
2152
|
+
```
|
|
2153
|
+
|
|
2154
|
+
---
|
|
2155
|
+
|
|
2156
|
+
## Shell-Specific Examples (cmd vs PowerShell)
|
|
2157
|
+
|
|
2158
|
+
### Windows cmd.exe
|
|
2159
|
+
|
|
2160
|
+
```ruby
|
|
2161
|
+
tool "mytool" do
|
|
2162
|
+
profile :windows_cmd,
|
|
2163
|
+
platform: :windows,
|
|
2164
|
+
shell: :cmd do
|
|
2165
|
+
# cmd.exe uses: /option value
|
|
2166
|
+
option_style :slash_space
|
|
2167
|
+
|
|
2168
|
+
# Special escaping for cmd (^ is escape char)
|
|
2169
|
+
escape_with :caret
|
|
2170
|
+
|
|
2171
|
+
# Environment variables: %VAR%
|
|
2172
|
+
env_var_format :percent
|
|
2173
|
+
end
|
|
2174
|
+
end
|
|
2175
|
+
```
|
|
2176
|
+
|
|
2177
|
+
**cmd.exe escaping:**
|
|
2178
|
+
```ruby
|
|
2179
|
+
# In cmd: paths with spaces need quotes
|
|
2180
|
+
MyTool.run(file: "C:\\Program Files\\input.txt")
|
|
2181
|
+
# CLI: mytool /Input "C:\Program Files\input.txt"
|
|
2182
|
+
|
|
2183
|
+
# Special characters need caret escaping
|
|
2184
|
+
MyTool.run(text: "hello & world")
|
|
2185
|
+
# CLI: mytool /Input "hello ^& world"
|
|
2186
|
+
```
|
|
2187
|
+
|
|
2188
|
+
### Windows PowerShell
|
|
2189
|
+
|
|
2190
|
+
```ruby
|
|
2191
|
+
tool "mytool" do
|
|
2192
|
+
profile :windows_powershell,
|
|
2193
|
+
platform: :windows,
|
|
2194
|
+
shell: :powershell do
|
|
2195
|
+
# PowerShell can use: --option=value (Unix-style)
|
|
2196
|
+
option_style :double_dash_equals
|
|
2197
|
+
|
|
2198
|
+
# Special escaping for PowerShell (` is escape char)
|
|
2199
|
+
escape_with :backtick
|
|
2200
|
+
|
|
2201
|
+
# Environment variables: $ENV:VAR
|
|
2202
|
+
env_var_format :env_prefix
|
|
2203
|
+
end
|
|
2204
|
+
end
|
|
2205
|
+
```
|
|
2206
|
+
|
|
2207
|
+
**PowerShell escaping:**
|
|
2208
|
+
```ruby
|
|
2209
|
+
# In PowerShell: backtick escaping for special chars
|
|
2210
|
+
MyTool.run(text: "hello `& world")
|
|
2211
|
+
# CLI: mytool --input "hello `& world"
|
|
2212
|
+
|
|
2213
|
+
# Environment variables
|
|
2214
|
+
env_vars do
|
|
2215
|
+
set "MY_VAR", from: :option
|
|
2216
|
+
# PowerShell: $ENV:MY_VAR = "value"
|
|
2217
|
+
end
|
|
2218
|
+
```
|
|
2219
|
+
|
|
2220
|
+
### Comparison Table
|
|
2221
|
+
|
|
2222
|
+
| Feature | Bash/Zsh | PowerShell | cmd.exe |
|
|
2223
|
+
|---------|----------|------------|---------|
|
|
2224
|
+
| Option style | `--flag=value` | `--flag=value` | `/flag value` |
|
|
2225
|
+
| Quote | `'` | `'` | `"` |
|
|
2226
|
+
| Escape char | `\` | `` ` `` | `^` |
|
|
2227
|
+
| Env var | `$VAR` | `$ENV:VAR` | `%VAR%` |
|
|
2228
|
+
| Path separator | `/` | `/` or `\` | `\` |
|
|
2229
|
+
| Special chars escape | `\` & \| > < | `` ` `` $ " | `^` & \| < > |
|
|
2230
|
+
|
|
2231
|
+
### Example: Same Command, Different Shells
|
|
2232
|
+
|
|
2233
|
+
```ruby
|
|
2234
|
+
class Converter < Ukiryu::Wrapper
|
|
2235
|
+
tool "convert" do
|
|
2236
|
+
names "convert"
|
|
2237
|
+
|
|
2238
|
+
command :process do
|
|
2239
|
+
option :input,
|
|
2240
|
+
type: :file,
|
|
2241
|
+
cli: "--input="
|
|
2242
|
+
|
|
2243
|
+
option :output,
|
|
2244
|
+
type: :file,
|
|
2245
|
+
cli: "--output="
|
|
2246
|
+
|
|
2247
|
+
option :quality,
|
|
2248
|
+
type: :integer,
|
|
2249
|
+
cli: "--quality="
|
|
2250
|
+
|
|
2251
|
+
flag :verbose, cli: "--verbose"
|
|
2252
|
+
|
|
2253
|
+
argument :files,
|
|
2254
|
+
type: :file,
|
|
2255
|
+
variadic: true
|
|
2256
|
+
end
|
|
2257
|
+
end
|
|
2258
|
+
end
|
|
2259
|
+
```
|
|
2260
|
+
|
|
2261
|
+
**Bash (macOS/Linux):**
|
|
2262
|
+
```ruby
|
|
2263
|
+
Converter.process(
|
|
2264
|
+
input: "diagram.svg",
|
|
2265
|
+
output: "result.png",
|
|
2266
|
+
quality: 95,
|
|
2267
|
+
verbose: true,
|
|
2268
|
+
files: ["overlay.png"]
|
|
2269
|
+
)
|
|
2270
|
+
# CLI: convert --input='diagram.svg' --output='result.png' --quality=95 --verbose 'overlay.png'
|
|
2271
|
+
```
|
|
2272
|
+
|
|
2273
|
+
**PowerShell:**
|
|
2274
|
+
```ruby
|
|
2275
|
+
Converter.process(
|
|
2276
|
+
input: "C:\\Files\\diagram.svg",
|
|
2277
|
+
output: "C:\\Files\\result.png",
|
|
2278
|
+
quality: 95,
|
|
2279
|
+
verbose: true,
|
|
2280
|
+
files: ["C:\\Files\\overlay.png"]
|
|
2281
|
+
)
|
|
2282
|
+
# CLI: convert --input='C:\Files\diagram.svg' --output='C:\Files\result.png' --quality=95 --verbose 'C:\Files\overlay.png'
|
|
2283
|
+
```
|
|
2284
|
+
|
|
2285
|
+
**cmd.exe:**
|
|
2286
|
+
```ruby
|
|
2287
|
+
Converter.process(
|
|
2288
|
+
input: "C:\\Files\\diagram.svg",
|
|
2289
|
+
output: "C:\\Files\\result.png",
|
|
2290
|
+
quality: 95,
|
|
2291
|
+
verbose: true,
|
|
2292
|
+
files: ["C:\\Files\\overlay.png"]
|
|
2293
|
+
)
|
|
2294
|
+
# CLI: convert /Input "C:\Files\diagram.svg" /Output "C:\Files\result.png" /Quality 95 /Verbose "C:\Files\overlay.png"
|
|
2295
|
+
```
|
|
2296
|
+
|
|
2297
|
+
---
|
|
2298
|
+
|
|
2299
|
+
## Command Declaration
|
|
2300
|
+
|
|
2301
|
+
```ruby
|
|
2302
|
+
class MyToolWrapper < Ukiryu::Wrapper
|
|
2303
|
+
tool "mytool" do
|
|
2304
|
+
# ==================================================
|
|
2305
|
+
# COMMAND: export
|
|
2306
|
+
# ==================================================
|
|
2307
|
+
command :export, desc: "Export to different format" do
|
|
2308
|
+
|
|
2309
|
+
# POSITIONAL argument (order matters!)
|
|
2310
|
+
argument :input,
|
|
2311
|
+
type: :file,
|
|
2312
|
+
position: 1,
|
|
2313
|
+
required: true
|
|
2314
|
+
|
|
2315
|
+
# OPTION (named parameter with value)
|
|
2316
|
+
option :format,
|
|
2317
|
+
type: :symbol,
|
|
2318
|
+
values: [:pdf, :eps, :svg, :png],
|
|
2319
|
+
cli: "--format=",
|
|
2320
|
+
required: true
|
|
2321
|
+
|
|
2322
|
+
# FLAG (boolean, presence indicates true)
|
|
2323
|
+
flag :plain,
|
|
2324
|
+
cli: "--plain",
|
|
2325
|
+
default: false
|
|
2326
|
+
|
|
2327
|
+
# OUTPUT (where output goes)
|
|
2328
|
+
output :file,
|
|
2329
|
+
type: :file,
|
|
2330
|
+
via: :option, # Uses --export-filename= option
|
|
2331
|
+
cli: "--export-filename=",
|
|
2332
|
+
position: :last
|
|
2333
|
+
end
|
|
2334
|
+
|
|
2335
|
+
# ==================================================
|
|
2336
|
+
# COMMAND: query
|
|
2337
|
+
# ==================================================
|
|
2338
|
+
command :query, desc: "Query document properties" do
|
|
2339
|
+
|
|
2340
|
+
argument :input,
|
|
2341
|
+
type: :file,
|
|
2342
|
+
required: true
|
|
2343
|
+
|
|
2344
|
+
flag :width,
|
|
2345
|
+
cli: "--query-width"
|
|
2346
|
+
|
|
2347
|
+
flag :height,
|
|
2348
|
+
cli: "--query-height"
|
|
2349
|
+
|
|
2350
|
+
# Parse output into hash
|
|
2351
|
+
parse_output do |stdout|
|
|
2352
|
+
{
|
|
2353
|
+
width: stdout[/Width:\s*(\d+)/, 1].to_i,
|
|
2354
|
+
height: stdout[/Height:\s*(\d+)/, 1].to_i
|
|
2355
|
+
}
|
|
2356
|
+
end
|
|
2357
|
+
end
|
|
2358
|
+
end
|
|
2359
|
+
```
|
|
2360
|
+
|
|
2361
|
+
### Parameter/Option/Flag Distinctions
|
|
2362
|
+
|
|
2363
|
+
| Concept | DSL | Description | Example |
|
|
2364
|
+
|---------|-----|-------------|--------|
|
|
2365
|
+
| **Positional Argument** | `argument` | Position in command line, no `--flag` | `input.pdf` |
|
|
2366
|
+
| **Variadic Argument** | `argument variadic: true` | Accepts multiple values (one or more) | `file1.pdf file2.pdf file3.pdf` |
|
|
2367
|
+
| **Option** | `option` | Named parameter with value, has `--flag=` syntax | `--format=pdf` |
|
|
2368
|
+
| **Flag** | `flag` | Boolean option (present/absent), no value | `--plain` |
|
|
2369
|
+
|
|
2370
|
+
### Variadic Arguments (Multiple Values)
|
|
2371
|
+
|
|
2372
|
+
Many CLI tools accept multiple values for a single argument position:
|
|
2373
|
+
|
|
2374
|
+
| Pattern | Example CLI | DSL |
|
|
2375
|
+
|---------|-------------|-----|
|
|
2376
|
+
| `command arg1 arg2*` | `cp file1 file2... dest` | `arg1` (pos 1), `arg2` (pos 2, variadic) |
|
|
2377
|
+
| `command arg1* arg2` | `convert input... output` | `arg1` (pos 1, variadic), `arg2` (pos 2) |
|
|
2378
|
+
| `command arg1 arg2* arg3` | `tar -cf archive files...` | `arg1`, `arg2*`, `arg3` with positions |
|
|
2379
|
+
| `command arg1*` | `cat files...` | Single variadic argument |
|
|
2380
|
+
| `command arg1 arg2*` (zero ok) | `git add [files...]` | `variadic: true, min: 0` |
|
|
2381
|
+
|
|
2382
|
+
#### DSL for Variadic Arguments
|
|
2383
|
+
|
|
2384
|
+
```ruby
|
|
2385
|
+
command :export do
|
|
2386
|
+
# Single argument (standard)
|
|
2387
|
+
argument :input,
|
|
2388
|
+
type: :file,
|
|
2389
|
+
position: 1,
|
|
2390
|
+
required: true
|
|
2391
|
+
|
|
2392
|
+
# Variadic: one or more files
|
|
2393
|
+
argument :sources,
|
|
2394
|
+
type: :file,
|
|
2395
|
+
position: 2,
|
|
2396
|
+
variadic: true # Default: min: 1
|
|
2397
|
+
|
|
2398
|
+
# Variadic: zero or more files (optional)
|
|
2399
|
+
argument :extras,
|
|
2400
|
+
type: :file,
|
|
2401
|
+
position: 3,
|
|
2402
|
+
variadic: true,
|
|
2403
|
+
min: 0
|
|
2404
|
+
|
|
2405
|
+
# Required argument after variadic (must come after)
|
|
2406
|
+
argument :output,
|
|
2407
|
+
type: :file,
|
|
2408
|
+
position: :last,
|
|
2409
|
+
required: true
|
|
2410
|
+
end
|
|
2411
|
+
```
|
|
2412
|
+
|
|
2413
|
+
#### Usage Examples
|
|
2414
|
+
|
|
2415
|
+
```ruby
|
|
2416
|
+
# Single variadic at end
|
|
2417
|
+
# CLI: inkscape output.pdf input1.svg input2.svg input3.svg
|
|
2418
|
+
Inkscape.export(
|
|
2419
|
+
output: "output.pdf",
|
|
2420
|
+
inputs: ["input1.svg", "input2.svg", "input3.svg"]
|
|
2421
|
+
)
|
|
2422
|
+
|
|
2423
|
+
# Variadic in middle
|
|
2424
|
+
# CLI: convert input1.jpg input2.jpg output.png
|
|
2425
|
+
Convert.convert(
|
|
2426
|
+
sources: ["input1.jpg", "input2.jpg"],
|
|
2427
|
+
output: "output.png"
|
|
2428
|
+
)
|
|
2429
|
+
|
|
2430
|
+
# Multiple variadic arguments
|
|
2431
|
+
# CLI: tool required.txt optional1.txt optional2.txt final.txt
|
|
2432
|
+
Tool.run(
|
|
2433
|
+
required: "required.txt",
|
|
2434
|
+
optional: ["optional1.txt", "optional2.txt"], # Can be empty []
|
|
2435
|
+
final: "final.txt"
|
|
2436
|
+
)
|
|
2437
|
+
|
|
2438
|
+
# Variadic with min: 0 (optional)
|
|
2439
|
+
# CLI: git add [files...]
|
|
2440
|
+
Git.add(
|
|
2441
|
+
files: ["file1.rb", "file2.rb"]
|
|
2442
|
+
)
|
|
2443
|
+
Git.add # No files - valid when min: 0
|
|
2444
|
+
```
|
|
2445
|
+
|
|
2446
|
+
#### Variadic Argument Rules
|
|
2447
|
+
|
|
2448
|
+
1. **Cardinality options:**
|
|
2449
|
+
- `variadic: true` - One or more values (default `min: 1`)
|
|
2450
|
+
- `variadic: true, min: 0` - Zero or more values (optional)
|
|
2451
|
+
- `variadic: true, min: 2` - At least N values (custom minimum)
|
|
2452
|
+
|
|
2453
|
+
2. **Position specification:**
|
|
2454
|
+
- Single variadic can use `position: N`
|
|
2455
|
+
- Multiple variadic arguments require explicit `position: N` for each
|
|
2456
|
+
- `position: :last` always comes after all other arguments
|
|
2457
|
+
|
|
2458
|
+
3. **Type validation:**
|
|
2459
|
+
- Each value in the array is validated individually
|
|
2460
|
+
- If any value fails validation, entire command fails before execution
|
|
2461
|
+
|
|
2462
|
+
4. **Command building:**
|
|
2463
|
+
- Variadic arguments expand to N CLI arguments in order
|
|
2464
|
+
- `inputs: ["a.svg", "b.svg"]` → `a.svg b.svg` in command line
|
|
2465
|
+
|
|
2466
|
+
#### Real-World Examples
|
|
2467
|
+
|
|
2468
|
+
**Copy command (cp):**
|
|
2469
|
+
```ruby
|
|
2470
|
+
class Cp < Ukiryu::Wrapper
|
|
2471
|
+
tool "cp" do
|
|
2472
|
+
names "cp"
|
|
2473
|
+
end
|
|
2474
|
+
|
|
2475
|
+
command :copy do
|
|
2476
|
+
# cp source... destination
|
|
2477
|
+
argument :sources,
|
|
2478
|
+
type: :file,
|
|
2479
|
+
position: 1,
|
|
2480
|
+
variadic: true,
|
|
2481
|
+
min: 1 # At least one source
|
|
2482
|
+
|
|
2483
|
+
argument :destination,
|
|
2484
|
+
type: :file,
|
|
2485
|
+
position: :last,
|
|
2486
|
+
required: true
|
|
2487
|
+
end
|
|
2488
|
+
end
|
|
2489
|
+
|
|
2490
|
+
# Usage:
|
|
2491
|
+
Cp.copy(sources: ["file1.txt", "file2.txt"], destination: "dest/")
|
|
2492
|
+
# CLI: cp file1.txt file2.txt dest/
|
|
2493
|
+
```
|
|
2494
|
+
|
|
2495
|
+
**ImageMagick convert (inputs before output):**
|
|
2496
|
+
```ruby
|
|
2497
|
+
class ImageMagick < Ukiryu::Wrapper
|
|
2498
|
+
tool "convert" do
|
|
2499
|
+
names "convert", "magick"
|
|
2500
|
+
end
|
|
2501
|
+
|
|
2502
|
+
command :convert do
|
|
2503
|
+
# convert input... [options] output
|
|
2504
|
+
argument :inputs,
|
|
2505
|
+
type: :file,
|
|
2506
|
+
position: 1,
|
|
2507
|
+
variadic: true,
|
|
2508
|
+
min: 1
|
|
2509
|
+
|
|
2510
|
+
option :resize,
|
|
2511
|
+
type: :string,
|
|
2512
|
+
cli: "-resize "
|
|
2513
|
+
|
|
2514
|
+
argument :output,
|
|
2515
|
+
type: :file,
|
|
2516
|
+
position: :last,
|
|
2517
|
+
required: true
|
|
2518
|
+
end
|
|
2519
|
+
end
|
|
2520
|
+
|
|
2521
|
+
# Usage:
|
|
2522
|
+
ImageMagick.convert(
|
|
2523
|
+
inputs: ["a.jpg", "b.jpg", "c.jpg"],
|
|
2524
|
+
resize: "50%",
|
|
2525
|
+
output: "combined.png"
|
|
2526
|
+
)
|
|
2527
|
+
# CLI: convert a.jpg b.jpg c.jpg -resize 50% combined.png
|
|
2528
|
+
```
|
|
2529
|
+
|
|
2530
|
+
**Git add (zero or more files):**
|
|
2531
|
+
```ruby
|
|
2532
|
+
class Git < Ukiryu::Wrapper
|
|
2533
|
+
tool "git" do
|
|
2534
|
+
names "git"
|
|
2535
|
+
end
|
|
2536
|
+
|
|
2537
|
+
command :add do
|
|
2538
|
+
argument :files,
|
|
2539
|
+
type: :file,
|
|
2540
|
+
position: 1,
|
|
2541
|
+
variadic: true,
|
|
2542
|
+
min: 0 # Optional: can add nothing to stage current changes
|
|
2543
|
+
flag :all, cli: "--all"
|
|
2544
|
+
flag :update, cli: "--update"
|
|
2545
|
+
end
|
|
2546
|
+
end
|
|
2547
|
+
|
|
2548
|
+
# Usage:
|
|
2549
|
+
Git.add(files: ["file1.rb", "file2.rb"])
|
|
2550
|
+
# CLI: git add file1.rb file2.rb
|
|
2551
|
+
|
|
2552
|
+
Git.add # No files, but valid
|
|
2553
|
+
# CLI: git add
|
|
2554
|
+
```
|
|
2555
|
+
|
|
2556
|
+
### Type System
|
|
2557
|
+
|
|
2558
|
+
All types with validation and escaping:
|
|
2559
|
+
|
|
2560
|
+
| Type | Description | Validation | Escaped | Example |
|
|
2561
|
+
|------|-------------|------------|---------|---------|
|
|
2562
|
+
| `:file` | File path | Must exist, platform-validated | Shell-specific | `file.pdf` |
|
|
2563
|
+
| `:string` | Text string | Custom validation | Shell-specific | `"hello"` |
|
|
2564
|
+
| `:integer` | Whole number | Range check | No escaping | `95` |
|
|
2565
|
+
| `:float` | Decimal | Custom validation | No escaping | `1.5` |
|
|
2566
|
+
| `:symbol` | Enum value | Must be in whitelist | No escaping | `:pdf` |
|
|
2567
|
+
| `:boolean` | Boolean flag | N/A | Flag presence | `--verbose` |
|
|
2568
|
+
| `:uri` | URL/URI | Must be valid URI | Shell-specific | `https://example.com` |
|
|
2569
|
+
| `:datetime` | Date/time | Must be parseable | Formatted, then escaped | `2025-01-21` |
|
|
2570
|
+
| `:hash` | Key-value pairs | Recursively escaped | Merge into env vars | `{key: "value"}` |
|
|
2571
|
+
| `:array` | Multiple values | Separator-based | Multiple arguments | `["a", "b"]` |
|
|
2572
|
+
|
|
2573
|
+
---
|
|
2574
|
+
|
|
2575
|
+
## Shell Classes
|
|
2576
|
+
|
|
2577
|
+
### Base Interface
|
|
2578
|
+
|
|
2579
|
+
```ruby
|
|
2580
|
+
module Ukiryu
|
|
2581
|
+
module Shell
|
|
2582
|
+
class Base
|
|
2583
|
+
# Identify the shell
|
|
2584
|
+
def name => Symbol
|
|
2585
|
+
|
|
2586
|
+
# Escape a string for this shell
|
|
2587
|
+
def escape(string) => String
|
|
2588
|
+
|
|
2589
|
+
# Quote an argument
|
|
2590
|
+
def quote(string) => String
|
|
2591
|
+
|
|
2592
|
+
# Format a file path
|
|
2593
|
+
def format_path(path) => String
|
|
2594
|
+
|
|
2595
|
+
# Format environment variable reference
|
|
2596
|
+
def env_var(name) => String
|
|
2597
|
+
|
|
2598
|
+
# Join executable and arguments
|
|
2599
|
+
def join(executable, *args) => String
|
|
2600
|
+
end
|
|
2601
|
+
end
|
|
2602
|
+
end
|
|
2603
|
+
```
|
|
2604
|
+
|
|
2605
|
+
### Bash Implementation
|
|
2606
|
+
|
|
2607
|
+
```ruby
|
|
2608
|
+
module Ukiryu
|
|
2609
|
+
module Shell
|
|
2610
|
+
class Bash < Base
|
|
2611
|
+
def name; :bash; end
|
|
2612
|
+
|
|
2613
|
+
def escape(string)
|
|
2614
|
+
# Single-quote string (literal)
|
|
2615
|
+
string.gsub("'") { "'\\''" }
|
|
2616
|
+
end
|
|
2617
|
+
|
|
2618
|
+
def quote(string)
|
|
2619
|
+
"'#{escape(string)}'"
|
|
2620
|
+
end
|
|
2621
|
+
|
|
2622
|
+
def format_path(path)
|
|
2623
|
+
path # Unix paths are fine
|
|
2624
|
+
end
|
|
2625
|
+
|
|
2626
|
+
def env_var(name)
|
|
2627
|
+
"$#{name}"
|
|
2628
|
+
end
|
|
2629
|
+
|
|
2630
|
+
def join(executable, *args)
|
|
2631
|
+
[executable, *args.map { |a| quote(a) }].join(" ")
|
|
2632
|
+
end
|
|
2633
|
+
end
|
|
2634
|
+
end
|
|
2635
|
+
end
|
|
2636
|
+
```
|
|
2637
|
+
|
|
2638
|
+
### PowerShell Implementation
|
|
2639
|
+
|
|
2640
|
+
```ruby
|
|
2641
|
+
module Ukiryu
|
|
2642
|
+
module Shell
|
|
2643
|
+
class PowerShell < Base
|
|
2644
|
+
def name; :powershell; end
|
|
2645
|
+
|
|
2646
|
+
def escape(string)
|
|
2647
|
+
# Backtick escaping for: ` $ "
|
|
2648
|
+
string.gsub(/[`"$]/) { "`$0" }
|
|
2649
|
+
end
|
|
2650
|
+
|
|
2651
|
+
def quote(string)
|
|
2652
|
+
"'#{escape(string)}'"
|
|
2653
|
+
end
|
|
2654
|
+
|
|
2655
|
+
def format_path(path)
|
|
2656
|
+
path # PowerShell handles forward slashes fine
|
|
2657
|
+
end
|
|
2658
|
+
|
|
2659
|
+
def env_var(name)
|
|
2660
|
+
"$ENV:#{name}"
|
|
2661
|
+
end
|
|
2662
|
+
|
|
2663
|
+
def join(executable, *args)
|
|
2664
|
+
[executable, *args.map { |a| quote(a) }].join(" ")
|
|
2665
|
+
end
|
|
2666
|
+
end
|
|
2667
|
+
end
|
|
2668
|
+
end
|
|
2669
|
+
```
|
|
2670
|
+
|
|
2671
|
+
### Cmd Implementation
|
|
2672
|
+
|
|
2673
|
+
```ruby
|
|
2674
|
+
module Ukiryu
|
|
2675
|
+
module Shell
|
|
2676
|
+
class Cmd < Base
|
|
2677
|
+
def name; :cmd; end
|
|
2678
|
+
|
|
2679
|
+
def escape(string)
|
|
2680
|
+
# Caret is escape character
|
|
2681
|
+
string.gsub(/[%^<>&|]/, '^^0')
|
|
2682
|
+
end
|
|
2683
|
+
|
|
2684
|
+
def quote(string)
|
|
2685
|
+
if string =~ /[ \t]/
|
|
2686
|
+
"\"#{escape(string)}\""
|
|
2687
|
+
else
|
|
2688
|
+
escape(string)
|
|
2689
|
+
end
|
|
2690
|
+
end
|
|
2691
|
+
|
|
2692
|
+
def format_path(path)
|
|
2693
|
+
# Convert to backslashes
|
|
2694
|
+
path.gsub('/', '\\')
|
|
2695
|
+
end
|
|
2696
|
+
|
|
2697
|
+
def env_var(name)
|
|
2698
|
+
"%#{name}%"
|
|
2699
|
+
end
|
|
2700
|
+
|
|
2701
|
+
def join(executable, *args)
|
|
2702
|
+
[executable, *args.map { |a| quote(a) }].join(" ")
|
|
2703
|
+
end
|
|
2704
|
+
end
|
|
2705
|
+
end
|
|
2706
|
+
end
|
|
2707
|
+
```
|
|
2708
|
+
|
|
2709
|
+
---
|
|
2710
|
+
|
|
2711
|
+
## Implementation Phases
|
|
2712
|
+
|
|
2713
|
+
### Phase 1: Foundation (6-8 weeks)
|
|
2714
|
+
- Core gem structure
|
|
2715
|
+
- Platform detection
|
|
2716
|
+
- Shell detection (EXPLICIT)
|
|
2717
|
+
- Basic type system (file, string, integer, symbol, boolean)
|
|
2718
|
+
- Basic command execution
|
|
2719
|
+
- Bash shell implementation
|
|
2720
|
+
|
|
2721
|
+
### Phase 2: Additional Types (3-4 weeks)
|
|
2722
|
+
- Float, URI, datetime, hash types
|
|
2723
|
+
- Array type with separators
|
|
2724
|
+
- Path validation (platform-specific)
|
|
2725
|
+
|
|
2726
|
+
### Phase 3: More Shells (3-4 weeks)
|
|
2727
|
+
- PowerShell shell implementation
|
|
2728
|
+
- Cmd shell implementation
|
|
2729
|
+
- Zsh fish implementation
|
|
2730
|
+
|
|
2731
|
+
### Phase 4: Profile System (4-6 weeks)
|
|
2732
|
+
- Profile DSL
|
|
2733
|
+
- Exact matching algorithm
|
|
2734
|
+
- Version compatibility
|
|
2735
|
+
- Profile error messages
|
|
2736
|
+
|
|
2737
|
+
### Phase 5: Complete DSL (4-6 weeks)
|
|
2738
|
+
- Argument/option/flag distinction
|
|
2739
|
+
- Position specification
|
|
2740
|
+
- Output specification
|
|
2741
|
+
- Conditional flags
|
|
2742
|
+
- Option mapping
|
|
2743
|
+
|
|
2744
|
+
### Phase 6: Testing & Docs (6-8 weeks)
|
|
2745
|
+
- GHA test matrix (platform × shell × ruby)
|
|
2746
|
+
- Shell-specific documentation
|
|
2747
|
+
- Platform-specific documentation
|
|
2748
|
+
- Example wrappers
|
|
2749
|
+
|
|
2750
|
+
### Phase 7: Vectory Migration (4-6 weeks)
|
|
2751
|
+
- Replace InkscapeWrapper
|
|
2752
|
+
- Replace GhostscriptWrapper
|
|
2753
|
+
- Full testing on all platforms
|
|
2754
|
+
|
|
2755
|
+
---
|
|
2756
|
+
|
|
2757
|
+
## Success Criteria
|
|
2758
|
+
|
|
2759
|
+
Ukiryu succeeds when:
|
|
2760
|
+
|
|
2761
|
+
1. ✅ **Explicit shell detection** - Raises clear error if shell unknown
|
|
2762
|
+
2. ✅ **Exact profile matching** - No fallbacks (version compatibility OK)
|
|
2763
|
+
3. ✅ **Type-safe parameters** - All types validated, shell-escaped automatically
|
|
2764
|
+
4. ✅ **Semantic validation** - Platform-specific paths rejected on wrong platforms
|
|
2765
|
+
5. ✅ **YAML profile registry** - Tool definitions maintained in YAML, not code
|
|
2766
|
+
6. ✅ **Schema validation** - All YAML profiles validated against JSON Schema
|
|
2767
|
+
7. ✅ **Profile inheritance** - Avoid duplication with YAML inheritance
|
|
2768
|
+
8. ✅ **Vectory replacement** - Full replacement of Vectory wrappers in 70% less code
|
|
2769
|
+
9. ✅ **GHA testing** - All platform×shell combinations tested
|
|
2770
|
+
10. ✅ **Complete documentation** - Separate docs for each shell/platform
|
|
2771
|
+
11. ✅ **Zero dependencies** - Ruby stdlib only
|
|
2772
|
+
12. ✅ **Argument/option/flag distinction** - Clear DSL concepts
|
|
2773
|
+
13. ✅ **PATH-only on Unix** - No hardcoded paths on Unix/macOS
|
|
2774
|
+
14. ✅ **Community registry** - Separate registry repo for tool profiles
|
|
2775
|
+
15. ✅ **Option format variations** - Support all CLI option formats
|
|
2776
|
+
16. ✅ **Value separators** - Comma, semicolon, colon, space, pipe, plus
|
|
2777
|
+
17. ✅ **Environment variables** - Per-command environment variable support
|
|
2778
|
+
18. ✅ **Subcommands** - Git-style subcommand support
|
|
2779
|
+
19. ✅ **Shell-specific examples** - cmd.exe vs PowerShell examples
|
|
2780
|
+
|
|
2781
|
+
---
|
|
2782
|
+
|
|
2783
|
+
## Key Insights
|
|
2784
|
+
|
|
2785
|
+
1. **YAML profiles over Ruby DSL**: Tool definitions should be maintained in YAML files, not Ruby code. This allows:
|
|
2786
|
+
- Non-developers to add/update tools
|
|
2787
|
+
- Faster iteration without gem releases
|
|
2788
|
+
- Community-contributed profile registry
|
|
2789
|
+
- Separation of framework (Ruby) from configuration (YAML)
|
|
2790
|
+
|
|
2791
|
+
2. **Hybrid architecture**:
|
|
2792
|
+
- **Ruby framework**: Shell detection, escaping, execution, validation
|
|
2793
|
+
- **YAML registry**: Tool definitions, versions, platform profiles
|
|
2794
|
+
- Users can still use Ruby DSL for custom tools not in registry
|
|
2795
|
+
|
|
2796
|
+
3. **"symbols in shell"**: User correctly pointed out that `:symbol` is a Ruby type, not a shell concept. In the shell, we pass strings. The `:symbol` type is for Ruby-level validation only.
|
|
2797
|
+
|
|
2798
|
+
4. **PATH environment**: On Unix, tools should be in PATH. No need to hardcode `/usr/bin`. Ukiryu should:
|
|
2799
|
+
- Always search `ENV["PATH"]` first
|
|
2800
|
+
- Add platform-specific paths only for platforms that need them (Windows app bundles, etc.)
|
|
2801
|
+
|
|
2802
|
+
5. **Option format variations**: CLI tools use many different formats:
|
|
2803
|
+
- `--flag=value` (double-dash equals)
|
|
2804
|
+
- `--flag value` (double-dash space)
|
|
2805
|
+
- `-f=value` (single-dash equals)
|
|
2806
|
+
- `-f value` (single-dash space)
|
|
2807
|
+
- `/flag value` (Windows slash)
|
|
2808
|
+
- `-r300` (embedded value)
|
|
2809
|
+
|
|
2810
|
+
6. **Value separators**: Many tools accept multiple values:
|
|
2811
|
+
- Comma: `--types=svg,png,pdf`
|
|
2812
|
+
- Semicolon: `--ids=obj1;obj2;obj3`
|
|
2813
|
+
- Colon: `-r300x600` (dimensions)
|
|
2814
|
+
- Space: `-I path1 path2`
|
|
2815
|
+
- Pipe, plus: for special cases
|
|
2816
|
+
|
|
2817
|
+
7. **Environment variables**: Per-command environment variables:
|
|
2818
|
+
- Inkscape: `DISPLAY=""` for headless on Unix
|
|
2819
|
+
- Ghostscript: `GS_LIB` for library path
|
|
2820
|
+
- ImageMagick: `MAGICK_CONFIGURE_PATH`
|
|
2821
|
+
|
|
2822
|
+
8. **Subcommands**: Git-style command structure:
|
|
2823
|
+
- `git add`, `git commit`, `git push`
|
|
2824
|
+
- Each subcommand has its own options
|
|
2825
|
+
- Shared options can be inherited
|
|
2826
|
+
|
|
2827
|
+
9. **Shell differences**:
|
|
2828
|
+
- **Bash/Zsh**: Single-quote escaping, `$VAR`, `/` paths
|
|
2829
|
+
- **PowerShell**: Backtick escaping, `$ENV:VAR`, `/` or `\` paths
|
|
2830
|
+
- **cmd.exe**: Caret escaping, `%VAR%`, `\` paths
|
|
2831
|
+
|
|
2832
|
+
10. **Explicit > Implicit**: Ukiryu must detect shell and profile explicitly, never guess.
|
|
2833
|
+
|
|
2834
|
+
11. **Type safety**: Users shouldn't deal with escaping at all—that's Ukiryu's job.
|
|
2835
|
+
|
|
2836
|
+
12. **Vectory as reference**: Vectory will be the first implementation driving Ukiryu development.
|
|
2837
|
+
|
|
2838
|
+
---
|
|
2839
|
+
|
|
2840
|
+
## Registry Organization
|
|
2841
|
+
|
|
2842
|
+
**Registry repo structure:**
|
|
2843
|
+
|
|
2844
|
+
```
|
|
2845
|
+
ukiryu-register/ # Community registry repo
|
|
2846
|
+
├── tools/ # Tool definitions (dir by command name)
|
|
2847
|
+
│ ├── inkscape/
|
|
2848
|
+
│ │ ├── 1.0.yaml # Version-specific profiles
|
|
2849
|
+
│ │ ├── 0.92.yaml
|
|
2850
|
+
│ │ └── 0.9.yaml
|
|
2851
|
+
│ ├── ghostscript/
|
|
2852
|
+
│ │ ├── 10.0.yaml
|
|
2853
|
+
│ │ ├── 9.5.yaml
|
|
2854
|
+
│ │ └── 9.0.yaml
|
|
2855
|
+
│ ├── imagemagick/
|
|
2856
|
+
│ │ ├── 7.0.yaml
|
|
2857
|
+
│ │ └── 6.0.yaml
|
|
2858
|
+
│ ├── git/
|
|
2859
|
+
│ │ ├── 2.45.yaml
|
|
2860
|
+
│ │ └── 2.40.yaml
|
|
2861
|
+
│ ├── docker/
|
|
2862
|
+
│ │ ├── 25.0.yaml
|
|
2863
|
+
│ │ └── 24.0.yaml
|
|
2864
|
+
│ └── ...
|
|
2865
|
+
├── schemas/ # YAML Schema files
|
|
2866
|
+
│ ├── tool-profile.yaml.schema
|
|
2867
|
+
│ ├── command-definition.yaml.schema
|
|
2868
|
+
│ └── registry.yaml.schema
|
|
2869
|
+
├── docs/ # AsciiDoc documentation
|
|
2870
|
+
│ ├── inkscape.adoc
|
|
2871
|
+
│ ├── ghostscript.adoc
|
|
2872
|
+
│ ├── imagemagick.adoc
|
|
2873
|
+
│ ├── git.adoc
|
|
2874
|
+
│ ├── contributing.adoc
|
|
2875
|
+
│ ├── registry.adoc
|
|
2876
|
+
│ └── README.adoc
|
|
2877
|
+
├── lib/ # Registry helper library
|
|
2878
|
+
│ └── ukiryu/
|
|
2879
|
+
│ └── registry.rb
|
|
2880
|
+
├── Gemfile # json-schema gem
|
|
2881
|
+
├── Rakefile # Validation tasks
|
|
2882
|
+
└── README.adoc # Registry overview
|
|
2883
|
+
```
|
|
2884
|
+
|
|
2885
|
+
**Ukiryu gem structure:**
|
|
2886
|
+
|
|
2887
|
+
```
|
|
2888
|
+
ukiryu/ # Ruby gem
|
|
2889
|
+
├── lib/
|
|
2890
|
+
│ ├── ukiryu.rb
|
|
2891
|
+
│ ├── ukiryu/
|
|
2892
|
+
│ │ ├── registry.rb # YAML loader from registry
|
|
2893
|
+
│ │ ├── tool.rb # Tool from YAML
|
|
2894
|
+
│ │ ├── shell.rb # Shell detection (EXPLICIT)
|
|
2895
|
+
│ │ ├── shell/
|
|
2896
|
+
│ │ │ ├── bash.rb
|
|
2897
|
+
│ │ │ ├── zsh.rb
|
|
2898
|
+
│ │ │ ├── powershell.rb
|
|
2899
|
+
│ │ │ └── cmd.rb
|
|
2900
|
+
│ │ ├── type.rb # Type validation
|
|
2901
|
+
│ │ ├── executor.rb # Command execution
|
|
2902
|
+
│ │ ├── version.rb # Version detection
|
|
2903
|
+
│ │ └── schema_validator.rb # JSON::Schema wrapper
|
|
2904
|
+
│ └── ukiryu.yml # Bundled profiles (optional)
|
|
2905
|
+
├── spec/
|
|
2906
|
+
│ └── ...
|
|
2907
|
+
├── Gemfile # json-schema dependency
|
|
2908
|
+
├── Rakefile
|
|
2909
|
+
└── README.adoc
|
|
2910
|
+
```
|
|
2911
|
+
|
|
2912
|
+
**Gemfile:**
|
|
2913
|
+
|
|
2914
|
+
```ruby
|
|
2915
|
+
# ukiryu-register/Gemfile
|
|
2916
|
+
source 'https://rubygems.org'
|
|
2917
|
+
|
|
2918
|
+
gem 'json-schema', '~> 3.0'
|
|
2919
|
+
gem 'yaml', '~> 0.3' # For schema validation
|
|
2920
|
+
```
|
|
2921
|
+
|
|
2922
|
+
**Registry README.adoc structure:**
|
|
2923
|
+
|
|
2924
|
+
```asciidoc
|
|
2925
|
+
= Ukiryu Tool Registry
|
|
2926
|
+
|
|
2927
|
+
== Overview
|
|
2928
|
+
|
|
2929
|
+
The Ukiryu Tool Registry maintains YAML profiles for command-line tools.
|
|
2930
|
+
|
|
2931
|
+
== Tools
|
|
2932
|
+
|
|
2933
|
+
* link:tools/inkscape/[Inkscape]
|
|
2934
|
+
* link:tools/ghostscript/[Ghostscript]
|
|
2935
|
+
* link:tools/imagemagick/[ImageMagick]
|
|
2936
|
+
|
|
2937
|
+
== Contributing
|
|
2938
|
+
|
|
2939
|
+
See link:contributing.adoc[Contributing Guidelines].
|
|
2940
|
+
|
|
2941
|
+
== Validation
|
|
2942
|
+
|
|
2943
|
+
Run `rake validate` to validate all tool profiles.
|
|
2944
|
+
```
|
|
2945
|
+
|
|
2946
|
+
---
|
|
2947
|
+
|
|
2948
|
+
## File Location
|
|
2949
|
+
|
|
2950
|
+
```
|
|
2951
|
+
docs/ukiryu-proposal.md
|
|
2952
|
+
```
|