planter-cli 0.0.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/.editorconfig +9 -0
- data/.gitignore +44 -0
- data/.irbrc +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +78 -0
- data/.travis.yml +7 -0
- data/.yardopts +8 -0
- data/CHANGELOG.md +45 -0
- data/Gemfile +6 -0
- data/Guardfile +25 -0
- data/LICENSE.txt +20 -0
- data/README.md +208 -0
- data/Rakefile +132 -0
- data/bin/plant +106 -0
- data/debug.log +0 -0
- data/docker/Dockerfile +12 -0
- data/docker/Dockerfile-2.6 +12 -0
- data/docker/Dockerfile-2.7 +12 -0
- data/docker/Dockerfile-3.0 +11 -0
- data/docker/bash_profile +15 -0
- data/docker/inputrc +57 -0
- data/lib/.rubocop.yml +1 -0
- data/lib/planter/array.rb +28 -0
- data/lib/planter/color.rb +370 -0
- data/lib/planter/errors.rb +59 -0
- data/lib/planter/file.rb +11 -0
- data/lib/planter/fileentry.rb +87 -0
- data/lib/planter/filelist.rb +144 -0
- data/lib/planter/hash.rb +103 -0
- data/lib/planter/plant.rb +228 -0
- data/lib/planter/prompt.rb +352 -0
- data/lib/planter/script.rb +59 -0
- data/lib/planter/string.rb +383 -0
- data/lib/planter/symbol.rb +28 -0
- data/lib/planter/version.rb +7 -0
- data/lib/planter.rb +222 -0
- data/planter-cli.gemspec +48 -0
- data/scripts/deploy.rb +97 -0
- data/scripts/runtests.sh +5 -0
- data/spec/.rubocop.yml +4 -0
- data/spec/planter/plant_spec.rb +14 -0
- data/spec/planter/string_spec.rb +20 -0
- data/spec/spec_helper.rb +20 -0
- data/src/_README.md +214 -0
- metadata +400 -0
@@ -0,0 +1,352 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Planter
|
4
|
+
# Individual question
|
5
|
+
module Prompt
|
6
|
+
# Class to prompt for answers
|
7
|
+
class Question
|
8
|
+
attr_reader :key, :type, :min, :max, :prompt, :gum, :condition, :default
|
9
|
+
|
10
|
+
##
|
11
|
+
## Initializes the given question.
|
12
|
+
##
|
13
|
+
## @param question [Hash] The question with key, prompt, and type,
|
14
|
+
## optionally default, min and max
|
15
|
+
##
|
16
|
+
## @return [Question] the question object
|
17
|
+
##
|
18
|
+
def initialize(question)
|
19
|
+
@key = question[:key].to_var
|
20
|
+
@type = question[:type].normalize_type
|
21
|
+
@min = question[:min]&.to_f || 1.0
|
22
|
+
@max = question[:max]&.to_f || 10.0
|
23
|
+
@prompt = question[:prompt] || nil
|
24
|
+
@default = question[:default]
|
25
|
+
@value = question[:value]
|
26
|
+
@gum = false # TTY::Which.exist?('gum')
|
27
|
+
end
|
28
|
+
|
29
|
+
##
|
30
|
+
## Ask the question, prompting for input based on type
|
31
|
+
##
|
32
|
+
## @return [Number, String] the response based on @type
|
33
|
+
##
|
34
|
+
def ask
|
35
|
+
return nil if @prompt.nil?
|
36
|
+
|
37
|
+
return @value.to_s.apply_variables.apply_regexes.coerce(@type) if @value
|
38
|
+
|
39
|
+
res = case @type
|
40
|
+
when :integer
|
41
|
+
read_number(integer: true)
|
42
|
+
when :float
|
43
|
+
read_number
|
44
|
+
when :date
|
45
|
+
if @value
|
46
|
+
date_default
|
47
|
+
else
|
48
|
+
read_date
|
49
|
+
end
|
50
|
+
when :class || :module
|
51
|
+
read_line.to_class_name
|
52
|
+
when :multiline
|
53
|
+
read_lines
|
54
|
+
else
|
55
|
+
read_line
|
56
|
+
end
|
57
|
+
Planter.notify("{dw}#{prompt}: {dy}#{res}{x}", :debug)
|
58
|
+
res
|
59
|
+
rescue TTY::Reader::InputInterrupt
|
60
|
+
raise Errors::InputError('Canceled')
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
##
|
66
|
+
## Read a numeric entry using gum or TTY::Reader
|
67
|
+
##
|
68
|
+
## @param integer [Boolean] Round result to nearest integer
|
69
|
+
##
|
70
|
+
## @return [Number] numeric response
|
71
|
+
##
|
72
|
+
def read_number(integer: false)
|
73
|
+
default = @default ? " {bw}[#{@default}]" : ''
|
74
|
+
Planter.notify("{by}#{@prompt} {xc}({bw}#{@min}{xc}-{bw}#{@max}{xc})#{default}")
|
75
|
+
|
76
|
+
res = @gum ? read_number_gum : read_line_tty
|
77
|
+
|
78
|
+
return @default unless res
|
79
|
+
|
80
|
+
res = integer ? res.to_f.round : res.to_f
|
81
|
+
|
82
|
+
res = read_number if res < @min || res > @max
|
83
|
+
res
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
## Parse a date value into a date string
|
88
|
+
##
|
89
|
+
## @return [String] date string
|
90
|
+
##
|
91
|
+
def date_default
|
92
|
+
default = @value || @default
|
93
|
+
return nil unless default
|
94
|
+
|
95
|
+
case default
|
96
|
+
when /^(no|ti)/
|
97
|
+
Time.now.strftime('%Y-%m-%d %H:%M')
|
98
|
+
when /^(to|da)/
|
99
|
+
Time.now.strftime('%Y-%m-%d')
|
100
|
+
when /^%/
|
101
|
+
Time.now.strftime(@default)
|
102
|
+
else
|
103
|
+
Chronic.parse(default).strftime('%Y-%m-%d')
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
##
|
108
|
+
## Accept a date string on the command line
|
109
|
+
##
|
110
|
+
## @param prompt [String] The prompt
|
111
|
+
##
|
112
|
+
## @return [Date] Parsed Date object
|
113
|
+
##
|
114
|
+
def read_date(prompt: nil)
|
115
|
+
prompt ||= @prompt
|
116
|
+
default = date_default
|
117
|
+
|
118
|
+
default = default ? " {bw}[#{default}]" : ''
|
119
|
+
Planter.notify("{by}#{prompt} (natural language)#{default}")
|
120
|
+
line = @gum ? read_line_gum : read_line_tty
|
121
|
+
return default unless line
|
122
|
+
|
123
|
+
Chronic.parse(line).strftime('%Y-%m-%d')
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
## Reads a line.
|
128
|
+
##
|
129
|
+
## @param prompt [String] If not nil, will trigger
|
130
|
+
## asking for a secondary response
|
131
|
+
## until a blank entry is given
|
132
|
+
##
|
133
|
+
## @return [String] the single-line response
|
134
|
+
##
|
135
|
+
def read_line(prompt: nil)
|
136
|
+
prompt ||= read_lines @prompt
|
137
|
+
default = @default ? " {bw}[#{@default}]" : ''
|
138
|
+
Planter.notify("{by}#{prompt}#{default}")
|
139
|
+
|
140
|
+
res = @gum ? read_line_gum : read_line_tty
|
141
|
+
|
142
|
+
return @default unless res
|
143
|
+
|
144
|
+
res
|
145
|
+
end
|
146
|
+
|
147
|
+
##
|
148
|
+
## Reads multiple lines.
|
149
|
+
##
|
150
|
+
## @param prompt [String] if not nil, will trigger
|
151
|
+
## asking for a secondary response
|
152
|
+
## until a blank entry is given
|
153
|
+
##
|
154
|
+
## @return [String] the multi-line response
|
155
|
+
##
|
156
|
+
def read_lines(prompt: nil)
|
157
|
+
prompt ||= @prompt
|
158
|
+
save = @gum ? 'Ctrl-J for newline, Enter' : 'Ctrl-D'
|
159
|
+
Planter.notify("{by}#{prompt} {xc}({bw}#{save}{xc} to save)'")
|
160
|
+
res = @gum ? read_multiline_gum(prompt) : read_mutliline_tty
|
161
|
+
|
162
|
+
return @default unless res
|
163
|
+
|
164
|
+
res.strip
|
165
|
+
end
|
166
|
+
|
167
|
+
##
|
168
|
+
## Read a numeric entry using gum
|
169
|
+
##
|
170
|
+
## @return [String] String response
|
171
|
+
##
|
172
|
+
def read_number_gum
|
173
|
+
trap('SIGINT') { exit! }
|
174
|
+
res = `gum input --placeholder "#{@min}-#{@max}"`.strip
|
175
|
+
return nil if res.strip.empty?
|
176
|
+
|
177
|
+
res
|
178
|
+
end
|
179
|
+
|
180
|
+
##
|
181
|
+
## Read a single line entry using TTY::Reader
|
182
|
+
##
|
183
|
+
## @return [String] String response
|
184
|
+
##
|
185
|
+
def read_line_tty
|
186
|
+
trap('SIGINT') { exit! }
|
187
|
+
reader = TTY::Reader.new
|
188
|
+
res = reader.read_line('>> ').strip
|
189
|
+
return nil if res.empty?
|
190
|
+
|
191
|
+
res
|
192
|
+
end
|
193
|
+
|
194
|
+
##
|
195
|
+
## Read a single line entry using gum
|
196
|
+
##
|
197
|
+
## @return [String] String response
|
198
|
+
##
|
199
|
+
def read_line_gum
|
200
|
+
trap('SIGINT') { exit! }
|
201
|
+
res = `gum input --placeholder "(blank to use default)"`.strip
|
202
|
+
return nil if res.empty?
|
203
|
+
|
204
|
+
res
|
205
|
+
end
|
206
|
+
|
207
|
+
##
|
208
|
+
## Read a multiline entry using TTY::Reader
|
209
|
+
##
|
210
|
+
## @return [string] multiline input
|
211
|
+
##
|
212
|
+
def read_mutliline_tty
|
213
|
+
trap('SIGINT') { exit! }
|
214
|
+
reader = TTY::Reader.new
|
215
|
+
res = reader.read_multiline
|
216
|
+
res.join("\n").strip
|
217
|
+
end
|
218
|
+
|
219
|
+
##
|
220
|
+
## Read a multiline entry using gum
|
221
|
+
##
|
222
|
+
## @return [string] multiline input
|
223
|
+
##
|
224
|
+
def read_multiline_gum(prompt)
|
225
|
+
trap('SIGINT') { exit! }
|
226
|
+
width = TTY::Screen.cols > 80 ? 80 : TTY::Screen.cols
|
227
|
+
`gum write --placeholder "#{prompt}" --width #{width} --char-limit 0`.strip
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
##
|
232
|
+
## Choose from an array of multiple choices. Letter surrounded in
|
233
|
+
## parenthesis becomes character for response. Only one letter should be
|
234
|
+
## specified and must be unique.
|
235
|
+
##
|
236
|
+
## @param choices [Array] The choices
|
237
|
+
## @param prompt [String] The prompt
|
238
|
+
## @param default_response [String] The character of the default
|
239
|
+
## response
|
240
|
+
##
|
241
|
+
## @return [String] character of selected response, lowercased
|
242
|
+
##
|
243
|
+
def self.choice(choices, prompt = 'Make a selection', default_response: nil)
|
244
|
+
$stdin.reopen('/dev/tty')
|
245
|
+
|
246
|
+
default = default_response.is_a?(String) ? default_response.downcase : nil
|
247
|
+
|
248
|
+
# if this isn't an interactive shell, answer default
|
249
|
+
return default unless $stdout.isatty
|
250
|
+
|
251
|
+
# If --defaults is set, return default
|
252
|
+
return default if Planter.accept_defaults
|
253
|
+
|
254
|
+
# clear the buffer
|
255
|
+
if ARGV&.length
|
256
|
+
ARGV.length.times do
|
257
|
+
ARGV.shift
|
258
|
+
end
|
259
|
+
end
|
260
|
+
system 'stty cbreak'
|
261
|
+
|
262
|
+
vertical = choices.join(' ').length + 4 > TTY::Screen.cols
|
263
|
+
desc = choices.map { |c| c.highlight_character(default: default) }
|
264
|
+
abbr = choices.abbr_choices(default: default)
|
265
|
+
|
266
|
+
options = if vertical
|
267
|
+
"{x}#{desc.join("\n")}\n{by}#{prompt}{x} #{abbr}{bw}? "
|
268
|
+
else
|
269
|
+
"{by}#{prompt}{bw}?\n#{desc.join(', ')}{x} #{abbr}:{x} "
|
270
|
+
end
|
271
|
+
|
272
|
+
$stdout.syswrite options.x
|
273
|
+
res = $stdin.sysread 1
|
274
|
+
puts
|
275
|
+
system 'stty cooked'
|
276
|
+
|
277
|
+
res.chomp!
|
278
|
+
res.downcase!
|
279
|
+
|
280
|
+
res.empty? ? default : res
|
281
|
+
end
|
282
|
+
|
283
|
+
def self.file_what?(entry)
|
284
|
+
options = %w[(o)vewrite (m)erge]
|
285
|
+
options << '(c)opy' unless File.exist?(entry.target)
|
286
|
+
options << '(i)gnore'
|
287
|
+
opt = Prompt.choice(options, "What do you want to do with #{File.basename(entry.target)}", default_response: 'i')
|
288
|
+
case opt
|
289
|
+
when /^m/
|
290
|
+
:merge
|
291
|
+
when /^o/
|
292
|
+
:overwrite
|
293
|
+
when /^c/
|
294
|
+
:copy
|
295
|
+
else
|
296
|
+
:ignore
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
##
|
301
|
+
## Ask a yes or no question in the terminal
|
302
|
+
##
|
303
|
+
## @param question [String] The question
|
304
|
+
## to ask
|
305
|
+
## @param default_response [Boolean] default
|
306
|
+
## response if no input
|
307
|
+
##
|
308
|
+
## @return [Boolean] yes or no
|
309
|
+
##
|
310
|
+
def self.yn(question, default_response: false)
|
311
|
+
$stdin.reopen('/dev/tty')
|
312
|
+
|
313
|
+
default = if default_response.is_a?(String)
|
314
|
+
default_response =~ /y/i ? true : false
|
315
|
+
else
|
316
|
+
default_response
|
317
|
+
end
|
318
|
+
|
319
|
+
# if this isn't an interactive shell, answer default
|
320
|
+
return default unless $stdout.isatty
|
321
|
+
|
322
|
+
# If --defaults is set, return default
|
323
|
+
return default if Planter.accept_defaults
|
324
|
+
|
325
|
+
# clear the buffer
|
326
|
+
if ARGV&.length
|
327
|
+
ARGV.length.times do
|
328
|
+
ARGV.shift
|
329
|
+
end
|
330
|
+
end
|
331
|
+
system 'stty cbreak'
|
332
|
+
|
333
|
+
options = if default.nil?
|
334
|
+
'{w}[{bw}y{w}/{bw}n{w}]'
|
335
|
+
else
|
336
|
+
"{w}[#{default ? '{bg}Y{w}/{bw}n' : '{bw}y{w}/{bg}N'}{w}]"
|
337
|
+
end
|
338
|
+
|
339
|
+
$stdout.syswrite "{bw}#{question.sub(/\?$/, '')} #{options}{bw}? {x}".x
|
340
|
+
res = $stdin.sysread 1
|
341
|
+
puts
|
342
|
+
system 'stty cooked'
|
343
|
+
|
344
|
+
res.chomp!
|
345
|
+
res.downcase!
|
346
|
+
|
347
|
+
return default if res.empty?
|
348
|
+
|
349
|
+
res =~ /y/i ? true : false
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Planter
|
4
|
+
# Script handler
|
5
|
+
class Script
|
6
|
+
attr_reader :script
|
7
|
+
|
8
|
+
##
|
9
|
+
## Initialize a Script object
|
10
|
+
##
|
11
|
+
## @param template_dir [String] Path to the current template dir
|
12
|
+
## @param output_dir [String] The planted template directory
|
13
|
+
## @param script [String] The script name
|
14
|
+
##
|
15
|
+
def initialize(template_dir, output_dir, script)
|
16
|
+
found = find_script(template_dir, output_dir, script)
|
17
|
+
Planter.notify("Script #{script} not found", :error, exit_code: 10) unless found
|
18
|
+
@script = found
|
19
|
+
|
20
|
+
Planter.notify("Directory #{output_dir} not found", :error, exit_code: 10) unless File.directory?(output_dir)
|
21
|
+
@template_directory = template_dir
|
22
|
+
@directory = output_dir
|
23
|
+
end
|
24
|
+
|
25
|
+
##
|
26
|
+
## Locate a script in either the base directory or template directory
|
27
|
+
##
|
28
|
+
## @param template_dir [String] The template dir
|
29
|
+
## @param script [String] The script name
|
30
|
+
##
|
31
|
+
## @return [String] Path to script
|
32
|
+
##
|
33
|
+
def find_script(template_dir, script)
|
34
|
+
parts = Shellwords.split(script).first
|
35
|
+
return script if File.exist?(parts[0])
|
36
|
+
|
37
|
+
if File.exist?(File.join(template_dir, '_scripts', parts[0]))
|
38
|
+
return "#{File.join(template_dir, '_scripts', parts[0])} #{parts[1..-1]}"
|
39
|
+
elsif File.exist?(File.join(BASE_DIR, 'scripts', parts[0]))
|
40
|
+
return "#{File.join(BASE_DIR, 'scripts', parts[0])} #{parts[1..-1]}"
|
41
|
+
end
|
42
|
+
|
43
|
+
nil
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
## Execute script, passing template directory and output directory as arguments $1 and $2
|
48
|
+
##
|
49
|
+
## @return [Boolean] true if success?
|
50
|
+
##
|
51
|
+
def run
|
52
|
+
`#{@script} "#{@template_directory}" "#{@directory}"`
|
53
|
+
|
54
|
+
Planter.notify("Error running #{File.basename(@script)}", :error, exit_code: 128) unless $?.success?
|
55
|
+
|
56
|
+
true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|