dry-cli-autocomplete 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.secrets.baseline +127 -0
- data/CHANGELOG.md +25 -0
- data/CLAUDE.md +88 -0
- data/LICENSE.txt +21 -0
- data/README.md +180 -0
- data/Rakefile +39 -0
- data/SPECIFICATION.md +246 -0
- data/docs/img/badge.svg +21 -0
- data/justfile +110 -0
- data/lefthook.yml +34 -0
- data/lib/dry/cli/autocomplete/command.rb +74 -0
- data/lib/dry/cli/autocomplete/emitters/bash.rb +177 -0
- data/lib/dry/cli/autocomplete/emitters/zsh.rb +213 -0
- data/lib/dry/cli/autocomplete/spec_builder.rb +103 -0
- data/lib/dry/cli/autocomplete/version.rb +15 -0
- data/lib/dry/cli/autocomplete.rb +19 -0
- data/lib/dry-cli-autocomplete.rb +3 -0
- data/sig/dry/cli/autocomplete.rbs +8 -0
- metadata +93 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/inflector"
|
|
4
|
+
|
|
5
|
+
module Dry
|
|
6
|
+
class CLI
|
|
7
|
+
module Autocomplete
|
|
8
|
+
module Emitters
|
|
9
|
+
# Turns a CompletionSpec into a native zsh `#compdef` script.
|
|
10
|
+
#
|
|
11
|
+
# This is not a bashcompinit shim. Options go through `_arguments`
|
|
12
|
+
# carrying their `desc` as help text, subcommands go through
|
|
13
|
+
# `_describe`, declared enum values become a `(a b c)` action, and a
|
|
14
|
+
# file argument becomes `_files`. That per-option help is the thing
|
|
15
|
+
# zsh users lose with a shim, and the reason this emitter exists.
|
|
16
|
+
# See SPECIFICATION.md §4.1.
|
|
17
|
+
#
|
|
18
|
+
# Shares no code with the bash emitter by design: a fourth shell
|
|
19
|
+
# should be a new class here, never another branch inside one of
|
|
20
|
+
# these two.
|
|
21
|
+
class Zsh
|
|
22
|
+
def self.call(spec)
|
|
23
|
+
new(spec).call
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(spec)
|
|
27
|
+
@spec = spec
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def call
|
|
31
|
+
"#{body.join("\n")}\n"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
attr_reader :spec
|
|
37
|
+
|
|
38
|
+
def body
|
|
39
|
+
header_lines + path_walk_lines + dispatch_lines + footer_lines
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def header_lines
|
|
43
|
+
[
|
|
44
|
+
"#compdef #{spec.program_name}",
|
|
45
|
+
"",
|
|
46
|
+
"# Generated by dry-cli-autocomplete. Regenerate after changing commands.",
|
|
47
|
+
"",
|
|
48
|
+
"#{function_name}() {",
|
|
49
|
+
' local curcontext="$curcontext" state line ret=1',
|
|
50
|
+
" local -a commands",
|
|
51
|
+
" typeset -A opt_args",
|
|
52
|
+
""
|
|
53
|
+
]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Resolves which node the cursor sits under by consuming words the
|
|
57
|
+
# registry knows, exactly as the bash emitter does. words[1] is the
|
|
58
|
+
# program itself under zsh, so the scan starts at 2.
|
|
59
|
+
def path_walk_lines
|
|
60
|
+
lines = [
|
|
61
|
+
' local path_key="" i=2',
|
|
62
|
+
" while (( i < CURRENT )); do",
|
|
63
|
+
' case "$path_key:${words[i]}" in'
|
|
64
|
+
]
|
|
65
|
+
edge_arms.each { |arm| lines << " #{arm}" }
|
|
66
|
+
lines + [
|
|
67
|
+
" (*) break ;;",
|
|
68
|
+
" esac",
|
|
69
|
+
" (( i += 1 ))",
|
|
70
|
+
" done",
|
|
71
|
+
""
|
|
72
|
+
]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def dispatch_lines
|
|
76
|
+
lines = [' case "$path_key" in']
|
|
77
|
+
spec.nodes.each { |node| lines.concat(node_arm_lines(node)) }
|
|
78
|
+
lines + [" esac", ""]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# A #compdef file dropped on $fpath is autoloaded by the completion
|
|
82
|
+
# system with its own name on the function stack, and is expected to
|
|
83
|
+
# do the completion. The same text sourced from a profile, which is
|
|
84
|
+
# what `eval "$(mycli completion zsh)"` does, runs with no completion
|
|
85
|
+
# in progress: calling the function there reaches for _describe
|
|
86
|
+
# before the completion system has defined it, and zsh reports a
|
|
87
|
+
# command-not-found from inside a file the user never wrote.
|
|
88
|
+
#
|
|
89
|
+
# Naming the function after the program, which is the convention a
|
|
90
|
+
# compdef file follows anyway, lets one line tell the two apart.
|
|
91
|
+
def footer_lines
|
|
92
|
+
[
|
|
93
|
+
" return ret",
|
|
94
|
+
"}",
|
|
95
|
+
"",
|
|
96
|
+
"if [ \"$funcstack[1]\" = \"#{function_name}\" ]; then",
|
|
97
|
+
" #{function_name} \"$@\"",
|
|
98
|
+
"else",
|
|
99
|
+
" compdef #{function_name} #{spec.program_name}",
|
|
100
|
+
"fi"
|
|
101
|
+
]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def node_arm_lines(node)
|
|
105
|
+
inner = arguments_call_lines(node) + describe_call_lines(node)
|
|
106
|
+
return [] if inner.empty?
|
|
107
|
+
|
|
108
|
+
[" (#{single_quote(path_key(node.path))})", *inner.map { |line| " #{line}" }, " ;;"]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# One `_arguments` call carrying every option and positional this
|
|
112
|
+
# node declares. `-s` allows clustered single-letter flags.
|
|
113
|
+
def arguments_call_lines(node)
|
|
114
|
+
specs = node.options.flat_map { |option| option_specs(option) } +
|
|
115
|
+
node.arguments.map { |argument| argument_spec(argument) }
|
|
116
|
+
return [] if specs.empty?
|
|
117
|
+
|
|
118
|
+
["_arguments -s \\", *specs[0..-2].map { |s| " #{s} \\" }, " #{specs.last} && ret=0"]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def describe_call_lines(node)
|
|
122
|
+
return [] if node.children.empty?
|
|
123
|
+
|
|
124
|
+
entries = node.children.map do |child|
|
|
125
|
+
single_quote("#{escape_describe(child)}:#{escape_describe(child_desc(node, child))}")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
["commands=(", *entries.map { |e| " #{e}" }, ")",
|
|
129
|
+
"_describe -t commands #{single_quote(describe_tag(node))} commands && ret=0"]
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Each alias gets its own spec rather than a {-f,--force} group:
|
|
133
|
+
# the grouped form needs its description outside the quotes, and
|
|
134
|
+
# one entry per flag is easier to read in the generated file.
|
|
135
|
+
def option_specs(option)
|
|
136
|
+
names = ["--#{option.name}"] + Array(option.aliases)
|
|
137
|
+
names.map { |name| single_quote("#{name}#{bracketed(option.desc)}#{option_action(option)}") }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# A boolean flag takes no value. Anything else gets one field
|
|
141
|
+
# naming what it wants and one supplying the completions for it.
|
|
142
|
+
def option_action(option)
|
|
143
|
+
return "" if option.boolean
|
|
144
|
+
|
|
145
|
+
":#{escape_spec(option.name)}:#{value_action(option.values)}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# A positional takes `:message:action`, with no bracketed
|
|
149
|
+
# description: zsh shows the message itself while completing, so
|
|
150
|
+
# the command's own wording goes there when it has any.
|
|
151
|
+
def argument_spec(argument)
|
|
152
|
+
action = argument.file ? "_files" : value_action(argument.values)
|
|
153
|
+
message = argument.desc.to_s.empty? ? argument.name : argument.desc
|
|
154
|
+
single_quote("*:#{escape_describe(message)}:#{action}")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def value_action(values)
|
|
158
|
+
return "" if values.nil? || values.empty?
|
|
159
|
+
|
|
160
|
+
"(#{values.map { |value| escape_spec(value) }.join(' ')})"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def bracketed(desc)
|
|
164
|
+
return "" if desc.nil? || desc.empty?
|
|
165
|
+
|
|
166
|
+
"[#{escape_bracket(desc)}]"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def child_desc(node, child)
|
|
170
|
+
found = spec.nodes.find { |candidate| candidate.path == node.path + [child] }
|
|
171
|
+
found&.desc.to_s
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def describe_tag(node)
|
|
175
|
+
node.path.empty? ? "command" : "#{node.path.join(' ')} command"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def path_key(path) = path.join(" ")
|
|
179
|
+
|
|
180
|
+
# zsh reads `]` as the end of a description and `:` as the start of
|
|
181
|
+
# the next spec field, so both have to be escaped inside one.
|
|
182
|
+
def escape_bracket(str) = str.to_s.gsub("\\", "\\\\\\\\").gsub("]", "\\\\]").gsub(":", "\\\\:")
|
|
183
|
+
|
|
184
|
+
def escape_spec(str) = str.to_s.gsub("\\", "\\\\\\\\").gsub(":", "\\\\:").gsub(/([()\[\] ])/, '\\\\\1')
|
|
185
|
+
|
|
186
|
+
def escape_describe(str) = str.to_s.gsub("\\", "\\\\\\\\").gsub(":", "\\\\:")
|
|
187
|
+
|
|
188
|
+
def single_quote(str) = "'#{str.to_s.gsub("'", "'\\\\''")}'"
|
|
189
|
+
|
|
190
|
+
# `_mycli`, not `_mycli_completions`: zsh autoloads a compdef file by
|
|
191
|
+
# the name of the function it defines, so matching that convention is
|
|
192
|
+
# what makes the footer's fpath check work.
|
|
193
|
+
def function_name = "_#{shell_identifier}"
|
|
194
|
+
|
|
195
|
+
# A program installed as `my-tool` cannot name a shell function
|
|
196
|
+
# directly. See SPECIFICATION.md §4.2.
|
|
197
|
+
def shell_identifier
|
|
198
|
+
Dry::Inflector.new.underscore(spec.program_name.to_s).gsub(/[^A-Za-z0-9_]/, "_")
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def edge_arms
|
|
202
|
+
spec.nodes.flat_map do |node|
|
|
203
|
+
node.children.map do |child|
|
|
204
|
+
"(#{single_quote("#{path_key(node.path)}:#{child}")}) " \
|
|
205
|
+
"path_key=#{single_quote(path_key(node.path + [child]))} ;;"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dry
|
|
4
|
+
class CLI
|
|
5
|
+
module Autocomplete
|
|
6
|
+
# Walks a Dry::CLI registry through its public API and returns a
|
|
7
|
+
# shell-agnostic description of every completion: one CompletionSpec
|
|
8
|
+
# carrying one Node per reachable, non-hidden command or group.
|
|
9
|
+
#
|
|
10
|
+
# Touches nothing beyond the registry and the command classes it
|
|
11
|
+
# already holds, so it stays cheap enough to run on every shell start.
|
|
12
|
+
# See SPECIFICATION.md §2.2 and §2.3.
|
|
13
|
+
class SpecBuilder
|
|
14
|
+
# `::Data`, with the leading colons, and never a bare `Data`. This file
|
|
15
|
+
# is lexically inside `module Dry`, so an unqualified constant is looked
|
|
16
|
+
# up there first: a bare `Struct` here meant `Dry::Struct` in any host
|
|
17
|
+
# that had dry-struct loaded, which took different arguments and broke
|
|
18
|
+
# on the host's machine while this gem's own suite stayed green. No
|
|
19
|
+
# `Dry::Data` exists today, but the same trap is one released gem away,
|
|
20
|
+
# and dry_struct_host_spec.rb is what keeps watch.
|
|
21
|
+
#
|
|
22
|
+
# Data rather than Struct because these are descriptions, built once and
|
|
23
|
+
# rendered: frozen is what they should be, a missing field raises rather
|
|
24
|
+
# than arriving as a silent nil, and `:values` stops colliding with a
|
|
25
|
+
# method Struct defines and Data does not.
|
|
26
|
+
CompletionSpec = ::Data.define(:program_name, :nodes)
|
|
27
|
+
Node = ::Data.define(:path, :desc, :options, :arguments, :children)
|
|
28
|
+
OptionSpec = ::Data.define(
|
|
29
|
+
:name, :type, :values, :aliases, :default, :desc, :required, :boolean, :array
|
|
30
|
+
)
|
|
31
|
+
ArgumentSpec = ::Data.define(:name, :values, :desc, :required, :file)
|
|
32
|
+
|
|
33
|
+
# A bare heuristic, used only when a host does not declare `file:`
|
|
34
|
+
# explicitly on the argument. See SPECIFICATION.md §4.3.
|
|
35
|
+
FILE_ARGUMENT_HEURISTIC = /file|path/i
|
|
36
|
+
|
|
37
|
+
def self.call(registry, program_name:)
|
|
38
|
+
new(registry, program_name).call
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def initialize(registry, program_name)
|
|
42
|
+
@registry = registry
|
|
43
|
+
@program_name = program_name
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def call
|
|
47
|
+
CompletionSpec.new(program_name: program_name, nodes: walk([]))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
attr_reader :registry, :program_name
|
|
53
|
+
|
|
54
|
+
def walk(path, nodes = [])
|
|
55
|
+
result = registry.get(path)
|
|
56
|
+
visible = visible_children(result)
|
|
57
|
+
|
|
58
|
+
nodes << build_node(path, result, visible)
|
|
59
|
+
visible.each_key { |name| walk(path + [name], nodes) }
|
|
60
|
+
nodes
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def visible_children(result)
|
|
64
|
+
result.children.reject { |_name, node| node.hidden }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def build_node(path, result, visible)
|
|
68
|
+
command = result.command
|
|
69
|
+
|
|
70
|
+
Node.new(
|
|
71
|
+
path: path,
|
|
72
|
+
desc: command&.description,
|
|
73
|
+
options: command ? command.options.map { |option| build_option(option) } : [],
|
|
74
|
+
arguments: command ? command.arguments.map { |argument| build_argument(argument) } : [],
|
|
75
|
+
children: visible.keys
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def build_option(option)
|
|
80
|
+
OptionSpec.new(
|
|
81
|
+
name: option.name.to_s, type: option.type, values: option.values,
|
|
82
|
+
aliases: option.aliases, default: option.default, desc: option.options[:desc],
|
|
83
|
+
required: option.required? || false, boolean: option.boolean?, array: option.array?
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_argument(argument)
|
|
88
|
+
ArgumentSpec.new(
|
|
89
|
+
name: argument.name.to_s, values: argument.values, desc: argument.options[:desc],
|
|
90
|
+
required: argument.required? || false, file: file_argument?(argument)
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def file_argument?(argument)
|
|
95
|
+
explicit = argument.options[:file]
|
|
96
|
+
return !!explicit unless explicit.nil?
|
|
97
|
+
|
|
98
|
+
argument.name.to_s.match?(FILE_ARGUMENT_HEURISTIC)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Dry
|
|
4
|
+
# Reopened rather than defined: dry-cli declares `class CLI`, not a module,
|
|
5
|
+
# and getting that wrong raises TypeError the moment both are loaded.
|
|
6
|
+
#
|
|
7
|
+
# Declared here without requiring dry-cli, because the gemspec loads this
|
|
8
|
+
# file at build time when the dependency may not be installed. dry-cli's CLI
|
|
9
|
+
# inherits from Object, so an empty reopening is compatible either way.
|
|
10
|
+
class CLI
|
|
11
|
+
module Autocomplete
|
|
12
|
+
VERSION = "0.1.3"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "dry/cli"
|
|
4
|
+
require_relative "autocomplete/version"
|
|
5
|
+
|
|
6
|
+
module Dry
|
|
7
|
+
class CLI
|
|
8
|
+
# Generates static shell completion scripts from a Dry::CLI registry.
|
|
9
|
+
#
|
|
10
|
+
# The generator reads the registry and never forces anything beyond it:
|
|
11
|
+
# commands, subcommands, options, their declared enum values, and which
|
|
12
|
+
# arguments look like file paths. Nothing it produces requires loading a
|
|
13
|
+
# host's data, because the script is regenerated on every shell start via
|
|
14
|
+
# `eval "$(mycli completion bash)"` and a slow generator is a slow shell.
|
|
15
|
+
module Autocomplete
|
|
16
|
+
class Error < StandardError; end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dry-cli-autocomplete
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.3
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Konstantin Gredeskoul
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: dry-cli
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: dry-inflector
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.0'
|
|
40
|
+
description: Supports auto-completion for dry-cli powered Ruby CLI tools, including
|
|
41
|
+
sub-commands, in BASH and ZSH.
|
|
42
|
+
email:
|
|
43
|
+
- kigster@gmail.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- ".secrets.baseline"
|
|
49
|
+
- CHANGELOG.md
|
|
50
|
+
- CLAUDE.md
|
|
51
|
+
- LICENSE.txt
|
|
52
|
+
- README.md
|
|
53
|
+
- Rakefile
|
|
54
|
+
- SPECIFICATION.md
|
|
55
|
+
- docs/img/badge.svg
|
|
56
|
+
- justfile
|
|
57
|
+
- lefthook.yml
|
|
58
|
+
- lib/dry-cli-autocomplete.rb
|
|
59
|
+
- lib/dry/cli/autocomplete.rb
|
|
60
|
+
- lib/dry/cli/autocomplete/command.rb
|
|
61
|
+
- lib/dry/cli/autocomplete/emitters/bash.rb
|
|
62
|
+
- lib/dry/cli/autocomplete/emitters/zsh.rb
|
|
63
|
+
- lib/dry/cli/autocomplete/spec_builder.rb
|
|
64
|
+
- lib/dry/cli/autocomplete/version.rb
|
|
65
|
+
- sig/dry/cli/autocomplete.rbs
|
|
66
|
+
homepage: https://github.com/kigster/dry-cli-autocomplete
|
|
67
|
+
licenses:
|
|
68
|
+
- MIT
|
|
69
|
+
metadata:
|
|
70
|
+
allowed_push_host: https://rubygems.org
|
|
71
|
+
homepage_uri: https://github.com/kigster/dry-cli-autocomplete
|
|
72
|
+
source_code_uri: https://github.com/kigster/dry-cli-autocomplete
|
|
73
|
+
changelog_uri: https://github.com/kigster/dry-cli-autocomplete/blob/main/CHANGELOG.md
|
|
74
|
+
rubygems_mfa_required: 'true'
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '4.0'
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 4.0.18
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: A missing auto-complete addition for dry-cli powered Ruby CLI tools for BASH
|
|
92
|
+
& ZSH
|
|
93
|
+
test_files: []
|