slipway 0.1.0
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/.yardopts +7 -0
- data/CHANGELOG.md +45 -0
- data/LICENSE.txt +21 -0
- data/README.md +1013 -0
- data/exe/slipway +10 -0
- data/lib/slipway/cli/builtins.rb +241 -0
- data/lib/slipway/cli/completer.rb +158 -0
- data/lib/slipway/cli/completion_scripts.rb +163 -0
- data/lib/slipway/cli/context.rb +67 -0
- data/lib/slipway/cli/errors.rb +19 -0
- data/lib/slipway/cli/globals.rb +27 -0
- data/lib/slipway/cli/help_renderer.rb +135 -0
- data/lib/slipway/cli/manpage.rb +226 -0
- data/lib/slipway/cli/parser.rb +45 -0
- data/lib/slipway/cli/registry.rb +191 -0
- data/lib/slipway/cli/runner.rb +186 -0
- data/lib/slipway/cli/style.rb +82 -0
- data/lib/slipway/cli/theme.rb +85 -0
- data/lib/slipway/cli/validator.rb +61 -0
- data/lib/slipway/cli.rb +22 -0
- data/lib/slipway/command_line.rb +22 -0
- data/lib/slipway/commands/api_resources.rb +82 -0
- data/lib/slipway/commands/apply.rb +172 -0
- data/lib/slipway/commands/base.rb +50 -0
- data/lib/slipway/commands/config.rb +73 -0
- data/lib/slipway/commands/create.rb +218 -0
- data/lib/slipway/commands/delete.rb +82 -0
- data/lib/slipway/commands/describe.rb +74 -0
- data/lib/slipway/commands/diff.rb +122 -0
- data/lib/slipway/commands/edit.rb +130 -0
- data/lib/slipway/commands/explain.rb +97 -0
- data/lib/slipway/commands/fetch.rb +112 -0
- data/lib/slipway/commands/from_dir.rb +141 -0
- data/lib/slipway/commands/get.rb +167 -0
- data/lib/slipway/commands/label.rb +114 -0
- data/lib/slipway/commands/manual.rb +67 -0
- data/lib/slipway/commands/options.rb +73 -0
- data/lib/slipway/commands/results.rb +57 -0
- data/lib/slipway/commands/rollout.rb +114 -0
- data/lib/slipway/commands/rollout_spec.rb +99 -0
- data/lib/slipway/commands/rollout_undo.rb +126 -0
- data/lib/slipway/commands/scope.rb +156 -0
- data/lib/slipway/commands/sync.rb +140 -0
- data/lib/slipway/commands.rb +54 -0
- data/lib/slipway/drift.rb +87 -0
- data/lib/slipway/editor.rb +71 -0
- data/lib/slipway/error.rb +27 -0
- data/lib/slipway/fetcher.rb +99 -0
- data/lib/slipway/field_selector.rb +86 -0
- data/lib/slipway/git/branch_name.rb +32 -0
- data/lib/slipway/git/commit.rb +13 -0
- data/lib/slipway/git/distance.rb +13 -0
- data/lib/slipway/git/errors.rb +125 -0
- data/lib/slipway/git/fake.rb +147 -0
- data/lib/slipway/git/fast_forward.rb +12 -0
- data/lib/slipway/git/fast_forwarding.rb +148 -0
- data/lib/slipway/git/fetch_result.rb +22 -0
- data/lib/slipway/git/move_back.rb +12 -0
- data/lib/slipway/git/reflog.rb +25 -0
- data/lib/slipway/git/repository.rb +288 -0
- data/lib/slipway/git/rolling_back.rb +98 -0
- data/lib/slipway/git/runner.rb +175 -0
- data/lib/slipway/git/status.rb +110 -0
- data/lib/slipway/git/url.rb +95 -0
- data/lib/slipway/git.rb +20 -0
- data/lib/slipway/inspector.rb +103 -0
- data/lib/slipway/labels.rb +126 -0
- data/lib/slipway/manifest.rb +265 -0
- data/lib/slipway/names.rb +22 -0
- data/lib/slipway/outcome.rb +45 -0
- data/lib/slipway/output/age.rb +70 -0
- data/lib/slipway/output/describe.rb +71 -0
- data/lib/slipway/output/explain.rb +75 -0
- data/lib/slipway/output/serializer.rb +35 -0
- data/lib/slipway/output/table.rb +67 -0
- data/lib/slipway/output.rb +28 -0
- data/lib/slipway/paths.rb +65 -0
- data/lib/slipway/plan.rb +227 -0
- data/lib/slipway/pool.rb +94 -0
- data/lib/slipway/resources.rb +91 -0
- data/lib/slipway/rollback.rb +236 -0
- data/lib/slipway/rollout_history.rb +69 -0
- data/lib/slipway/runtime.rb +65 -0
- data/lib/slipway/scanner.rb +54 -0
- data/lib/slipway/schema.rb +128 -0
- data/lib/slipway/selector.rb +146 -0
- data/lib/slipway/settings.rb +174 -0
- data/lib/slipway/state.rb +82 -0
- data/lib/slipway/store.rb +170 -0
- data/lib/slipway/syncer.rb +139 -0
- data/lib/slipway/version.rb +5 -0
- data/lib/slipway/views/group.rb +35 -0
- data/lib/slipway/views/project.rb +148 -0
- data/lib/slipway/views.rb +10 -0
- data/lib/slipway/yaml.rb +14 -0
- data/lib/slipway.rb +32 -0
- data/man/man1/slipway-api-resources.1 +53 -0
- data/man/man1/slipway-apply.1 +45 -0
- data/man/man1/slipway-completion.1 +29 -0
- data/man/man1/slipway-config-path.1 +20 -0
- data/man/man1/slipway-config-view.1 +25 -0
- data/man/man1/slipway-config.1 +22 -0
- data/man/man1/slipway-create.1 +89 -0
- data/man/man1/slipway-delete.1 +46 -0
- data/man/man1/slipway-describe.1 +95 -0
- data/man/man1/slipway-diff.1 +120 -0
- data/man/man1/slipway-edit.1 +34 -0
- data/man/man1/slipway-explain.1 +36 -0
- data/man/man1/slipway-fetch.1 +77 -0
- data/man/man1/slipway-get.1 +155 -0
- data/man/man1/slipway-help.1 +19 -0
- data/man/man1/slipway-label.1 +53 -0
- data/man/man1/slipway-man.1 +36 -0
- data/man/man1/slipway-rollout-history.1 +28 -0
- data/man/man1/slipway-rollout-pause.1 +21 -0
- data/man/man1/slipway-rollout-resume.1 +21 -0
- data/man/man1/slipway-rollout-undo.1 +73 -0
- data/man/man1/slipway-rollout-unpin.1 +21 -0
- data/man/man1/slipway-rollout.1 +36 -0
- data/man/man1/slipway-sync.1 +90 -0
- data/man/man1/slipway-version.1 +17 -0
- data/man/man1/slipway.1 +243 -0
- metadata +173 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'errors'
|
|
4
|
+
require_relative 'style'
|
|
5
|
+
require_relative 'theme'
|
|
6
|
+
require_relative 'globals'
|
|
7
|
+
require_relative 'parser'
|
|
8
|
+
require_relative 'validator'
|
|
9
|
+
require_relative 'help_renderer'
|
|
10
|
+
require_relative 'builtins'
|
|
11
|
+
|
|
12
|
+
module Slipway
|
|
13
|
+
module CLI
|
|
14
|
+
# Returns the exit status and never lets an exception escape as a backtrace.
|
|
15
|
+
class Runner
|
|
16
|
+
SIGINT_STATUS = 130
|
|
17
|
+
EPIPE_STATUS = 0
|
|
18
|
+
UNEXPECTED_STATUS = 1
|
|
19
|
+
COLOR_VARIABLE = 'SLIPWAY_COLOR'
|
|
20
|
+
THEME_VARIABLE = 'SLIPWAY_THEME'
|
|
21
|
+
DEBUG_VARIABLE = 'SLIPWAY_DEBUG'
|
|
22
|
+
COLOR_FLAG = "--#{Globals::COLOR.long}".freeze
|
|
23
|
+
COLOR_INLINE = "#{COLOR_FLAG}=".freeze
|
|
24
|
+
|
|
25
|
+
# `color` and `theme` are the fallbacks used when neither a flag nor the environment
|
|
26
|
+
# decides, which is where the configuration file values arrive.
|
|
27
|
+
def initialize(registry, context, color: nil, theme: nil)
|
|
28
|
+
@registry = registry
|
|
29
|
+
@context = context
|
|
30
|
+
@default_color = color
|
|
31
|
+
@default_theme = theme
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# The Context drops output once a stream's reader is gone, so a broken pipe reaching here
|
|
35
|
+
# comes from a handler writing past the Context; it ends the run quietly.
|
|
36
|
+
def run(argv)
|
|
37
|
+
execute(argv.dup)
|
|
38
|
+
rescue Errno::EPIPE
|
|
39
|
+
EPIPE_STATUS
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
# Color is settled first, from `--color` typed anywhere on the line or from the
|
|
45
|
+
# environment, so even an error in the parse that follows is painted the way the user asked.
|
|
46
|
+
def execute(argv)
|
|
47
|
+
@flag_color = flag_color(argv)
|
|
48
|
+
colorize({})
|
|
49
|
+
dispatch(argv)
|
|
50
|
+
rescue Errno::EPIPE
|
|
51
|
+
raise
|
|
52
|
+
rescue Slipway::Error => e
|
|
53
|
+
report(e.problems, e.hint, layout: e.is_a?(UsageError))
|
|
54
|
+
e.exit_status
|
|
55
|
+
rescue Interrupt
|
|
56
|
+
@context.err.write("\n")
|
|
57
|
+
SIGINT_STATUS
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
report_unexpected(e)
|
|
60
|
+
UNEXPECTED_STATUS
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def dispatch(argv)
|
|
64
|
+
values = {}
|
|
65
|
+
command, path, rest = walk(argv, values)
|
|
66
|
+
return run_raw(command, rest) if command.raw
|
|
67
|
+
|
|
68
|
+
args = parse(@registry.globals + command.options, path, values) { it.permute!(rest) }
|
|
69
|
+
opts = defaults(command, values)
|
|
70
|
+
return show_help(command, path) if opts[:help]
|
|
71
|
+
return show_version if opts[:version]
|
|
72
|
+
return show_help(command, path) if command.group?
|
|
73
|
+
|
|
74
|
+
Validator.new(command, path, @registry).call(args, opts)
|
|
75
|
+
invoke(command, path, args, opts)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Globals are parsed before every word, so they may appear before the verb and between a
|
|
79
|
+
# group and its subcommand.
|
|
80
|
+
def walk(argv, values)
|
|
81
|
+
command = @registry.root
|
|
82
|
+
path = []
|
|
83
|
+
while command.group?
|
|
84
|
+
argv = parse(@registry.globals, path, values) { it.order!(argv) }
|
|
85
|
+
word = argv.shift
|
|
86
|
+
return [command, path, argv] if word.nil?
|
|
87
|
+
|
|
88
|
+
command = command.find(word) or raise unknown_command(word, path)
|
|
89
|
+
path << word
|
|
90
|
+
end
|
|
91
|
+
[command, path, argv]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def unknown_command(word, path)
|
|
95
|
+
UsageError.new(@registry.unknown_command(word, path), hint: @registry.run_hint(path))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Color is re-resolved after every parse step, so a `--color` the parser has just
|
|
99
|
+
# read applies to the error that may follow it.
|
|
100
|
+
def parse(options, path, values)
|
|
101
|
+
parser = Parser.new(options, values)
|
|
102
|
+
yield parser
|
|
103
|
+
rescue OptionParser::InvalidOption => e
|
|
104
|
+
raise UsageError.new(@registry.unknown_option(e.args.first, options), hint: @registry.help_hint(path))
|
|
105
|
+
rescue OptionParser::ParseError => e
|
|
106
|
+
raise UsageError.new(e.message, hint: @registry.help_hint(path))
|
|
107
|
+
ensure
|
|
108
|
+
colorize(values)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def defaults(command, values)
|
|
112
|
+
Parser.new(@registry.globals + command.options, values).defaults.freeze
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Taken as typed; the Validator reports an invalid value once the parse reaches it.
|
|
116
|
+
def flag_color(argv)
|
|
117
|
+
argv.take_while { it != '--' }.reverse_each do |word|
|
|
118
|
+
return Globals::COLOR.implicit if word == COLOR_FLAG
|
|
119
|
+
return word.delete_prefix(COLOR_INLINE) if word.start_with?(COLOR_INLINE)
|
|
120
|
+
end
|
|
121
|
+
nil
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def colorize(values)
|
|
125
|
+
@context = @context.with_color(values.fetch(:color) { @flag_color || fallback_color }, theme:)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def fallback_color
|
|
129
|
+
@fallback_color ||= environment(COLOR_VARIABLE, Style::MODES) || @default_color || Style::DEFAULT_MODE
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def theme
|
|
133
|
+
@theme ||= Theme.fetch(environment(THEME_VARIABLE, Theme::NAMES) || @default_theme || Theme::DEFAULT_NAME)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# An invalid value is reported the way the configuration file reports the same key.
|
|
137
|
+
def environment(name, allowed = nil)
|
|
138
|
+
value = @context.env[name]
|
|
139
|
+
return nil if value.to_s.empty?
|
|
140
|
+
return value if allowed.nil? || allowed.include?(value)
|
|
141
|
+
|
|
142
|
+
raise Slipway::Error, "#{name}: must be one of #{allowed.join(', ')}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def invoke(command, path, args, opts)
|
|
146
|
+
command.handler.call(@context, args, opts)
|
|
147
|
+
0
|
|
148
|
+
rescue UsageError => e
|
|
149
|
+
raise if e.hint
|
|
150
|
+
|
|
151
|
+
raise UsageError.new(e.message, hint: @registry.help_hint(path))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def run_raw(command, rest)
|
|
155
|
+
command.handler.call(@context, rest, {})
|
|
156
|
+
0
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def show_help(command, path)
|
|
160
|
+
renderer = HelpRenderer.new(@registry, @context.style)
|
|
161
|
+
@context.print(command.equal?(@registry.root) ? renderer.root : renderer.command(command, path))
|
|
162
|
+
0
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def show_version
|
|
166
|
+
@context.puts(Builtins.version_line(@registry.program, @registry.version))
|
|
167
|
+
0
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# A problem can quote git's stderr, a manifest field, a path or a typed word, so nothing reaches
|
|
171
|
+
# stderr raw. Only a usage error keeps its line feeds, for its "Did you mean this?" list.
|
|
172
|
+
def report(problems, hint, layout: false)
|
|
173
|
+
prefix = @context.paint_err(:error, 'error:')
|
|
174
|
+
problems.each { @context.warn("#{prefix} #{Style.plain(it, layout:)}") }
|
|
175
|
+
@context.warn(Style.plain(hint)) if hint
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def report_unexpected(error)
|
|
179
|
+
report([error.message], nil)
|
|
180
|
+
return unless environment(DEBUG_VARIABLE)
|
|
181
|
+
|
|
182
|
+
@context.warn(error.class.name, *Array(error.backtrace).map { " #{it}" })
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slipway
|
|
4
|
+
module CLI
|
|
5
|
+
class Style
|
|
6
|
+
MODES = %w[auto always never].freeze
|
|
7
|
+
DEFAULT_MODE = 'auto'
|
|
8
|
+
FORCE_VARIABLES = %w[FORCE_COLOR CLICOLOR_FORCE].freeze
|
|
9
|
+
# C0 and C1 control characters, DEL, and the bidirectional embedding, override and isolate
|
|
10
|
+
# controls: everything a terminal would obey, or use to reorder a line, instead of show.
|
|
11
|
+
CONTROL = /[\x00-\x1F\x7F\u0080-\u009F\u202A-\u202E\u2066-\u2069]/
|
|
12
|
+
REPLACEMENT = "\uFFFD"
|
|
13
|
+
LAYOUT = "\t\n"
|
|
14
|
+
|
|
15
|
+
attr_reader :theme
|
|
16
|
+
|
|
17
|
+
def self.for(mode, tty:, env:, theme: Theme.default)
|
|
18
|
+
new(enabled: enabled?(mode, tty:, env:), theme:)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.enabled?(mode, tty:, env:)
|
|
22
|
+
case mode
|
|
23
|
+
when 'always' then true
|
|
24
|
+
when 'never' then false
|
|
25
|
+
else auto?(tty:, env:)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.auto?(tty:, env:)
|
|
30
|
+
return false if set?(env['NO_COLOR'])
|
|
31
|
+
return true if FORCE_VARIABLES.any? { set?(env[it]) }
|
|
32
|
+
return false if env['TERM'] == 'dumb'
|
|
33
|
+
|
|
34
|
+
tty
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.set?(value) = !value.to_s.empty?
|
|
38
|
+
|
|
39
|
+
private_class_method :enabled?, :auto?, :set?
|
|
40
|
+
|
|
41
|
+
def self.disabled = new(enabled: false, theme: Theme.default)
|
|
42
|
+
|
|
43
|
+
# Painting is how slipway's own escape sequences reach a stream. Text from anywhere else is
|
|
44
|
+
# made visible, so a commit subject, a git message or a manifest field can neither move the
|
|
45
|
+
# cursor nor paint a STATUS of its own: C0 characters and DEL take caret notation
|
|
46
|
+
# (ESC is ^[, DEL is ^?), the rest the replacement character. `layout` keeps line feeds and
|
|
47
|
+
# tabs, for a message laid out on several lines such as "Did you mean this?". Under the C
|
|
48
|
+
# locale Ruby hands ARGV, ENV and paths over as BINARY, which a UTF-8 pattern cannot match;
|
|
49
|
+
# reading the bytes as UTF-8 keeps the valid ones and scrubs the rest.
|
|
50
|
+
def self.plain(text, layout: false)
|
|
51
|
+
String.new(text.to_s, encoding: Encoding::UTF_8).scrub.gsub(CONTROL) do |char|
|
|
52
|
+
next char if layout && LAYOUT.include?(char)
|
|
53
|
+
|
|
54
|
+
char.ord < 0x80 ? "^#{((char.ord + 0x40) & 0x7F).chr}" : REPLACEMENT
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def initialize(enabled:, theme:)
|
|
59
|
+
@enabled = enabled
|
|
60
|
+
@theme = theme
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def enabled? = @enabled
|
|
64
|
+
|
|
65
|
+
def paint(role, text)
|
|
66
|
+
wrap(theme.sgr(role), text)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def paint_cycle(role, index, text)
|
|
70
|
+
wrap(theme.sgr_at(role, index), text)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def wrap(sgr, text)
|
|
76
|
+
return text.to_s unless @enabled
|
|
77
|
+
|
|
78
|
+
"\e[#{sgr}m#{text}\e[0m"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slipway
|
|
4
|
+
module CLI
|
|
5
|
+
# The presets follow kubecolor's dark and light themes.
|
|
6
|
+
class Theme
|
|
7
|
+
# Roles whose value is a list are cycled by an index (column or nesting depth).
|
|
8
|
+
DARK = {
|
|
9
|
+
table_header: '1',
|
|
10
|
+
table_columns: %w[37 36],
|
|
11
|
+
describe_keys: %w[96 36],
|
|
12
|
+
explain_required: '31',
|
|
13
|
+
string: '93',
|
|
14
|
+
number: '35',
|
|
15
|
+
boolean_true: '32',
|
|
16
|
+
boolean_false: '31',
|
|
17
|
+
none: '90;3',
|
|
18
|
+
status_success: '32',
|
|
19
|
+
status_warning: '33',
|
|
20
|
+
status_danger: '31',
|
|
21
|
+
muted: '90;3',
|
|
22
|
+
help_header: '1',
|
|
23
|
+
help_flag: '36',
|
|
24
|
+
help_command: '32',
|
|
25
|
+
help_comment: '90;3',
|
|
26
|
+
apply_created: '32',
|
|
27
|
+
apply_configured: '33',
|
|
28
|
+
apply_unchanged: '35',
|
|
29
|
+
create_created: '32',
|
|
30
|
+
delete_deleted: '31',
|
|
31
|
+
label_labeled: '32',
|
|
32
|
+
label_unlabeled: '33',
|
|
33
|
+
label_not_labeled: '90;3',
|
|
34
|
+
result_changed: '32',
|
|
35
|
+
result_unchanged: '35',
|
|
36
|
+
result_skipped: '33',
|
|
37
|
+
result_paused: '90;3',
|
|
38
|
+
result_denied: '31',
|
|
39
|
+
result_failed: '31',
|
|
40
|
+
dry_run: '36',
|
|
41
|
+
error: '31',
|
|
42
|
+
warning: '33'
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
# White becomes black, cyan blue and bright yellow plain yellow, to read on a light background.
|
|
46
|
+
LIGHT = DARK.merge(
|
|
47
|
+
table_columns: %w[30 34],
|
|
48
|
+
describe_keys: %w[94 34],
|
|
49
|
+
string: '33',
|
|
50
|
+
help_flag: '34',
|
|
51
|
+
dry_run: '34'
|
|
52
|
+
).freeze
|
|
53
|
+
|
|
54
|
+
PRESETS = { 'dark' => DARK, 'light' => LIGHT }.freeze
|
|
55
|
+
NAMES = PRESETS.keys.freeze
|
|
56
|
+
DEFAULT_NAME = 'dark'
|
|
57
|
+
|
|
58
|
+
attr_reader :name
|
|
59
|
+
|
|
60
|
+
# Callers check `name` against NAMES first, so an unknown name here is a bug.
|
|
61
|
+
def self.fetch(name)
|
|
62
|
+
new(name, PRESETS.fetch(name))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.default = fetch(DEFAULT_NAME)
|
|
66
|
+
|
|
67
|
+
def initialize(name, roles)
|
|
68
|
+
@name = name
|
|
69
|
+
@roles = roles
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def sgr(role)
|
|
73
|
+
value = @roles.fetch(role)
|
|
74
|
+
raise ArgumentError, "role #{role.inspect} is a list; use sgr_at" if value.is_a?(Array)
|
|
75
|
+
|
|
76
|
+
value
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def sgr_at(role, index)
|
|
80
|
+
values = Array(@roles.fetch(role))
|
|
81
|
+
values[index % values.size]
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Slipway
|
|
4
|
+
module CLI
|
|
5
|
+
class Validator
|
|
6
|
+
def initialize(command, path, registry)
|
|
7
|
+
@command = command
|
|
8
|
+
@path = path
|
|
9
|
+
@registry = registry
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def call(args, opts)
|
|
13
|
+
check_arity(args)
|
|
14
|
+
check_positional_enums(args)
|
|
15
|
+
check_required(opts)
|
|
16
|
+
check_option_enums(opts)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def check_arity(args)
|
|
22
|
+
if args.size < @command.min_args
|
|
23
|
+
missing = @command.positionals[args.size]
|
|
24
|
+
usage_error("missing required argument #{missing.name.inspect}")
|
|
25
|
+
end
|
|
26
|
+
max = @command.max_args
|
|
27
|
+
usage_error("unexpected argument #{args[max].inspect}") if max && args.size > max
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def check_positional_enums(args)
|
|
31
|
+
args.each_with_index do |arg, index|
|
|
32
|
+
positional = @command.positional_at(index)
|
|
33
|
+
next unless positional&.enum && !positional.enum.include?(arg)
|
|
34
|
+
|
|
35
|
+
usage_error("invalid argument #{arg.inspect} for #{positional.name}: #{allowed(positional.enum)}")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def check_required(opts)
|
|
40
|
+
@command.options.select(&:required).each do |opt|
|
|
41
|
+
usage_error("required flag(s) #{"--#{opt.long}".inspect} not set") if opts[opt.key].nil?
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def check_option_enums(opts)
|
|
46
|
+
(@registry.globals + @command.options).select(&:enum).each do |opt|
|
|
47
|
+
value = opts[opt.key]
|
|
48
|
+
next if value.nil? || opt.enum.include?(value)
|
|
49
|
+
|
|
50
|
+
usage_error("invalid argument #{value.inspect} for --#{opt.long}: #{allowed(opt.enum)}")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def allowed(values) = "must be one of #{values.join(', ')}"
|
|
55
|
+
|
|
56
|
+
def usage_error(message)
|
|
57
|
+
raise UsageError.new(message, hint: @registry.help_hint(@path))
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/slipway/cli.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'did_you_mean'
|
|
4
|
+
require_relative 'cli/errors'
|
|
5
|
+
require_relative 'cli/theme'
|
|
6
|
+
require_relative 'cli/style'
|
|
7
|
+
require_relative 'cli/context'
|
|
8
|
+
require_relative 'cli/registry'
|
|
9
|
+
require_relative 'cli/globals'
|
|
10
|
+
require_relative 'cli/parser'
|
|
11
|
+
require_relative 'cli/validator'
|
|
12
|
+
require_relative 'cli/help_renderer'
|
|
13
|
+
require_relative 'cli/completer'
|
|
14
|
+
require_relative 'cli/completion_scripts'
|
|
15
|
+
require_relative 'cli/manpage'
|
|
16
|
+
require_relative 'cli/builtins'
|
|
17
|
+
require_relative 'cli/runner'
|
|
18
|
+
|
|
19
|
+
module Slipway
|
|
20
|
+
module CLI
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
require_relative 'paths'
|
|
5
|
+
|
|
6
|
+
module Slipway
|
|
7
|
+
# The git command lines slipway prints for the user to run. Slipway never runs them.
|
|
8
|
+
module CommandLine
|
|
9
|
+
# Joins `words` as given, so a literal such as @{upstream}..HEAD prints as it is typed; a
|
|
10
|
+
# caller quotes any word a manifest or git supplied, as Plan does.
|
|
11
|
+
def self.git(project, *words) = ['git', '-C', path(project.path), *words].join(' ')
|
|
12
|
+
|
|
13
|
+
# The path as the manifest writes it, so the line matches what was registered. Only the part
|
|
14
|
+
# after ~ is quoted, since a shell does not expand a quoted ~.
|
|
15
|
+
def self.path(path)
|
|
16
|
+
rest = path.sub(Paths::TILDE, '')
|
|
17
|
+
return Shellwords.escape(path) if rest == path
|
|
18
|
+
|
|
19
|
+
rest.empty? ? '~' : "~#{Shellwords.escape(rest)}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'base'
|
|
4
|
+
|
|
5
|
+
module Slipway
|
|
6
|
+
module Commands
|
|
7
|
+
class APIResources < Base
|
|
8
|
+
DESCRIPTION = "Print the supported resource types.\n\n" \
|
|
9
|
+
'Prints a table of the resource types slipway knows, sorted by name. A command accepts a type ' \
|
|
10
|
+
'by its NAME, its singular or one of its SHORTNAMES. Type words are case-insensitive. Use -o ' \
|
|
11
|
+
'wide to add the verbs that act on each type, and -o name to print only the names.'
|
|
12
|
+
COLUMNS = {
|
|
13
|
+
'NAME' => 'The name of the type, in the plural.',
|
|
14
|
+
'SHORTNAMES' => 'The aliases of the type, besides its singular.',
|
|
15
|
+
'KIND' => 'The kind a manifest of the type declares.',
|
|
16
|
+
'GROUPED' => 'Whether resources of the type live in a group, so that -n and -A scope them. kubectl\'s ' \
|
|
17
|
+
'NAMESPACED column says the same about namespaces.',
|
|
18
|
+
'VERBS' => 'The commands that act on resources of the type.'
|
|
19
|
+
}.freeze
|
|
20
|
+
WIDE_HEADERS = COLUMNS.keys.freeze
|
|
21
|
+
HEADERS = (WIDE_HEADERS - ['VERBS']).freeze
|
|
22
|
+
GLOSSARIES = [
|
|
23
|
+
CLI::Glossary.new(title: 'Columns',
|
|
24
|
+
intro: 'Every type shows NAME, SHORTNAMES, KIND and GROUPED, and -o wide adds VERBS. A ' \
|
|
25
|
+
"type without aliases reads #{Output::Table::NONE} in SHORTNAMES.",
|
|
26
|
+
entries: COLUMNS)
|
|
27
|
+
].freeze
|
|
28
|
+
OUTPUT = Options::OUTPUT.with(enum: [Output::TABLE, Output::WIDE, Output::NAME])
|
|
29
|
+
|
|
30
|
+
def self.command(factory)
|
|
31
|
+
CLI::Command.new(
|
|
32
|
+
name: 'api-resources', summary: 'Print the supported resource types', section: 'Other Commands',
|
|
33
|
+
description: DESCRIPTION, examples:, glossaries: GLOSSARIES, options: [OUTPUT, Options::NO_HEADERS],
|
|
34
|
+
handler: new(factory)
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.examples
|
|
39
|
+
[
|
|
40
|
+
CLI::Example.new(comment: 'Print the supported resource types', command: 'api-resources'),
|
|
41
|
+
CLI::Example.new(comment: 'Print the supported resource types with more information',
|
|
42
|
+
command: 'api-resources -o wide'),
|
|
43
|
+
CLI::Example.new(comment: 'Print the names of the supported resource types', command: 'api-resources -o name')
|
|
44
|
+
]
|
|
45
|
+
end
|
|
46
|
+
private_class_method :examples
|
|
47
|
+
|
|
48
|
+
def run(_runtime, context, _args, opts)
|
|
49
|
+
types = Resources::KINDS.sort_by(&:plural)
|
|
50
|
+
case opts[:output]
|
|
51
|
+
when Output::NAME then types.each { context.puts(it.plural) }
|
|
52
|
+
when Output::WIDE
|
|
53
|
+
verbs = verbs_by_kind
|
|
54
|
+
table(context, opts, WIDE_HEADERS, types.map { [*row(it), verbs.fetch(it)] })
|
|
55
|
+
else table(context, opts, HEADERS, types.map { row(it) })
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def row(kind) = [kind.plural, kind.aliases.join(','), kind.title, kind.namespaced?]
|
|
62
|
+
|
|
63
|
+
# The registry that holds this verb is built after it, so the verbs are built again from
|
|
64
|
+
# VERBS to read what each one acts on.
|
|
65
|
+
def verbs_by_kind
|
|
66
|
+
commands = VERBS.map { it.command(@factory) }
|
|
67
|
+
Resources::KINDS.to_h { |kind| [kind, commands.select { acts_on?(it, kind) }.map(&:name).sort.join(',')] }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# A group such as rollout acts on a kind when one of its subcommands does.
|
|
71
|
+
def acts_on?(command, kind)
|
|
72
|
+
return command.subcommands.any? { acts_on?(it, kind) } if command.group?
|
|
73
|
+
|
|
74
|
+
command.handler.kinds.include?(kind)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def table(context, opts, headers, rows)
|
|
78
|
+
Output::Table.new(context, headers:, show_headers: !opts[:no_headers]).print(rows)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|