planter-cli 3.0.4 → 3.0.7

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Planter
4
+ ## Configuration class
5
+ class Config < Hash
6
+ attr_reader :template
7
+
8
+ ##
9
+ ## Initialize a new Config object for a template
10
+ ##
11
+ def initialize
12
+ super()
13
+
14
+ @config = initial_config
15
+ @template = Planter.template
16
+
17
+ load_template
18
+
19
+ die('No configuration found', :config) unless @config
20
+
21
+ generate_accessors
22
+ end
23
+
24
+ ## String representation of the configuration
25
+ def to_s
26
+ @config.to_s
27
+ end
28
+
29
+ ## Get a config key
30
+ ##
31
+ ## @param key [String,Symbol] key
32
+ ##
33
+ ## @return [String] value
34
+ ##
35
+ def [](key)
36
+ @config[key]
37
+ end
38
+
39
+ ##
40
+ ## Set a config option
41
+ ##
42
+ ## @param key [String,Symbol] key
43
+ ## @param value [String] value
44
+ ##
45
+ def []=(key, value)
46
+ @config[key.to_sym] = value
47
+ generate_accessors
48
+ end
49
+
50
+ private
51
+
52
+ ## Default configuration
53
+ ##
54
+ ## @return [Hash] default configuration
55
+ ##
56
+ ## @api private
57
+ ##
58
+ def initial_config
59
+ {
60
+ defaults: false,
61
+ git_init: false,
62
+ files: { '_planter.yml' => 'ignore' },
63
+ color: true,
64
+ preserve_tags: nil,
65
+ variables: nil,
66
+ replacements: nil,
67
+ repo: false,
68
+ patterns: nil,
69
+ debug: false,
70
+ script: nil
71
+ }
72
+ end
73
+
74
+ ## Generate accessors for configuration
75
+ ##
76
+ ## @api private
77
+ ##
78
+ def generate_accessors
79
+ @config.each do |k, v|
80
+ define_singleton_method(k) { v } unless respond_to?(k)
81
+ end
82
+ end
83
+
84
+ ##
85
+ ## Build a configuration from template name
86
+ ##
87
+ ## @return [Hash] Configuration object
88
+ ##
89
+ ## @api private
90
+ ##
91
+ def load_template
92
+ Planter.variables ||= {}
93
+ FileUtils.mkdir_p(Planter.base_dir) unless File.directory?(Planter.base_dir)
94
+ base_config = File.join(Planter.base_dir, 'planter.yml')
95
+
96
+ if File.exist?(base_config)
97
+ @config = @config.deep_merge(YAML.load(IO.read(base_config)).symbolize_keys)
98
+ else
99
+ default_base_config = {
100
+ defaults: false,
101
+ git_init: false,
102
+ files: { '_planter.yml' => 'ignore' },
103
+ color: true,
104
+ preserve_tags: true
105
+ }
106
+ begin
107
+ File.open(base_config, 'w') { |f| f.puts(YAML.dump(default_base_config.stringify_keys)) }
108
+ rescue Errno::ENOENT
109
+ Planter.notify("Unable to create #{base_config}", :error)
110
+ end
111
+ @config = @config.deep_merge(default_base_config).symbolize_keys
112
+ Planter.notify("New configuration written to #{base_config}, edit as needed.", :warn)
113
+ end
114
+
115
+ base_dir = File.join(Planter.base_dir, 'templates', @template)
116
+ unless File.directory?(base_dir)
117
+ notify("Template #{@template} does not exist", :error)
118
+ res = Prompt.yn('Create template directory', default_response: false)
119
+
120
+ die('Canceled') unless res
121
+
122
+ FileUtils.mkdir_p(base_dir)
123
+ end
124
+
125
+ load_template_config
126
+
127
+ config_array_to_hash(:files) if @config[:files].is_a?(Array)
128
+ config_array_to_hash(:replacements) if @config[:replacements].is_a?(Array)
129
+ rescue Psych::SyntaxError => e
130
+ die("Parse error in configuration file:\n#{e.message}", :config)
131
+ end
132
+
133
+ ##
134
+ ## Load a template-specific configuration
135
+ ##
136
+ ## @return [Hash] updated config object
137
+ ##
138
+ ## @api private
139
+ ##
140
+ def load_template_config
141
+ base_dir = File.join(Planter.base_dir, 'templates', @template)
142
+ config = File.join(base_dir, '_planter.yml')
143
+
144
+ unless File.exist?(config)
145
+ default_config = {
146
+ variables: [
147
+ key: 'var_key',
148
+ prompt: 'CLI Prompt',
149
+ type: '[string, float, integer, number, date]',
150
+ value: '(optional, force value, can include variables. Empty to prompt. For date type: today, now, etc.)',
151
+ default: '(optional default value, leave empty or remove key for no default)',
152
+ min: '(optional, for number type set a minimum value)',
153
+ max: '(optional, for number type set a maximum value)'
154
+ ],
155
+ git_init: false,
156
+ files: {
157
+ '*.tmp' => 'ignore',
158
+ '*.bak' => 'ignore',
159
+ '.DS_Store' => 'ignore'
160
+ }
161
+ }
162
+ FileUtils.mkdir_p(base_dir)
163
+ File.open(config, 'w') { |f| f.puts(YAML.dump(default_config.stringify_keys)) }
164
+ Planter.notify("New configuration written to #{config}, please edit.", :warn)
165
+ Process.exit 0
166
+ end
167
+ @config = @config.deep_merge(YAML.load(IO.read(config)).symbolize_keys)
168
+ end
169
+
170
+ ##
171
+ ## Convert an errant array to a hash
172
+ ##
173
+ ## @param key [Symbol] The key in @config to convert
174
+ ##
175
+ ## @api private
176
+ ##
177
+ def config_array_to_hash(key)
178
+ files = {}
179
+ @config[key].each do |k|
180
+ files[k.keys.first] = k.values.first
181
+ end
182
+ @config[key] = files
183
+ end
184
+ end
185
+ end
@@ -6,8 +6,14 @@ module Planter
6
6
  # Operation to execute on the file
7
7
  attr_accessor :operation
8
8
 
9
- # File path and target path
10
- attr_reader :file, :target, :tags
9
+ # File path
10
+ attr_reader :file
11
+
12
+ # Target path
13
+ attr_reader :target
14
+
15
+ # Tags
16
+ attr_reader :tags
11
17
 
12
18
  ##
13
19
  ## Initialize a FileEntry object
@@ -19,7 +25,7 @@ module Planter
19
25
  ## @return [FileEntry] a Hash of parameters
20
26
  ##
21
27
  def initialize(file, target, operation)
22
- return nil unless File.exist?(file)
28
+ return unless File.exist?(file)
23
29
 
24
30
  @file = file
25
31
  @target = target
@@ -50,6 +50,10 @@ module Planter
50
50
  ##
51
51
  ## @param entry [FileEntry] The file entry
52
52
  ##
53
+ ## @return [Boolean] success
54
+ ##
55
+ ## @api private
56
+ ##
53
57
  def handle_operator(entry)
54
58
  case entry.operation
55
59
  when :ignore
@@ -65,8 +69,17 @@ module Planter
65
69
  apply_tags(entry)
66
70
  end
67
71
 
72
+ #
73
+ # Apply tags to the target file from the source file
74
+ #
75
+ # @param [FileEntry] entry
76
+ #
77
+ # @return [Boolean] success
78
+ #
79
+ # @api private
80
+ #
68
81
  def apply_tags(entry)
69
- return unless Planter.config[:preserve_tags]
82
+ return unless Planter.config.preserve_tags
70
83
 
71
84
  Tag.copy(entry.file, entry.target) if File.exist?(entry.target)
72
85
  end
@@ -76,6 +89,8 @@ module Planter
76
89
  ##
77
90
  ## @return [Boolean] success
78
91
  ##
92
+ ## @api private
93
+ ##
79
94
  def prepare_copy
80
95
  @files.each do |entry|
81
96
  if entry.matches_pattern?
@@ -90,6 +105,10 @@ module Planter
90
105
  ##
91
106
  ## @param entry [FileEntry] The file entry
92
107
  ##
108
+ ## @return [Boolean] success
109
+ ##
110
+ ## @api private
111
+ ##
93
112
  def propogate_operation(entry)
94
113
  @files.each do |file|
95
114
  file.operation = entry.operation if file.file =~ /^#{entry.file}/
@@ -104,6 +123,8 @@ module Planter
104
123
  ##
105
124
  ## @return [Boolean] success
106
125
  ##
126
+ ## @api private
127
+ ##
107
128
  def merge(entry)
108
129
  return copy_file(entry) if File.directory?(entry.file)
109
130
 
@@ -157,7 +178,12 @@ module Planter
157
178
  ##
158
179
  ## @return [Boolean] success
159
180
  ##
181
+ ## @api private
182
+ ##
160
183
  def copy_file(file, overwrite: false)
184
+ # If the target file already exists and overwrite is true,
185
+ # or Planter.overwrite is true, then delete the target file
186
+ FileUtils.rm_rf(file.target) if (overwrite || Planter.overwrite) && File.exist?(file.target)
161
187
  # Check if the target file already exists
162
188
  # If it does and overwrite is true, or Planter.overwrite is true,
163
189
  # or if the file doesn't exist, then copy the file
@@ -165,7 +191,7 @@ module Planter
165
191
  # Make sure the target directory exists
166
192
  FileUtils.mkdir_p(File.dirname(file.target))
167
193
  # Copy the file if it isn't a directory
168
- FileUtils.cp(file.file, file.target) unless File.directory?(file.file)
194
+ FileUtils.cp_r(file.file, file.target) unless File.directory?(file.file)
169
195
  # Log a message to the console
170
196
  Planter.notify("[Copied] #{file.file} => #{file.target}", :debug, above_spinner: true)
171
197
  # Return true to indicate success
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Planter
4
+ # Integer extensions
5
+ class ::Integer
6
+ # Clean value (dummy method)
7
+ def clean_value
8
+ self
9
+ end
10
+
11
+ # Has selector (dummy method)
12
+ def selector?
13
+ true
14
+ end
15
+
16
+ # Highlight character
17
+ def highlight_character(default: nil)
18
+ "(#{self})".highlight_character(default: default)
19
+ end
20
+ end
21
+
22
+ # Float extensions
23
+ class ::Float
24
+ # Clean value (dummy method)
25
+ def clean_value
26
+ self
27
+ end
28
+
29
+ # Has selector (dummy method)
30
+ def selector?
31
+ true
32
+ end
33
+
34
+ # Highlight character
35
+ def highlight_character(default: nil)
36
+ "(#{self})".highlight_character(default: default)
37
+ end
38
+ end
39
+ end
data/lib/planter/plant.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  module Planter
4
4
  # Primary class
5
5
  class Plant
6
+ attr_reader :config
7
+
6
8
  ##
7
9
  ## Initialize a new Plant object
8
10
  ##
@@ -11,20 +13,24 @@ module Planter
11
13
  ##
12
14
  def initialize(template = nil, variables = nil)
13
15
  Planter.variables = variables if variables.is_a?(Hash)
14
- Planter.config = template if template
16
+ # Planter.config = template if template
17
+ template ||= Planter.template
18
+ die('No template specified', :config) unless template
19
+
20
+ @config = Planter::Config.new
15
21
 
16
- @basedir = File.join(Planter.base_dir, 'templates', Planter.template)
22
+ @basedir = File.join(Planter.base_dir, 'templates', @config.template)
17
23
  @target = Planter.target || Dir.pwd
18
24
 
19
- @git = Planter.config[:git_init] || false
25
+ @git = @config.git_init || false
20
26
  @debug = Planter.debug
21
- @repo = Planter.config[:repo] || false
27
+ @repo = @config.repo || false
22
28
 
23
29
  # Coerce any existing variables (like from the command line) to the types
24
30
  # defined in configuration
25
31
  coerced = {}
26
32
  Planter.variables.each do |k, v|
27
- cfg_var = Planter.config[:variables].select { |var| k = var[:key] }
33
+ cfg_var = @config.variables.select { |var| k = var[:key] }
28
34
  next unless cfg_var.count.positive?
29
35
 
30
36
  var = cfg_var.first
@@ -34,7 +40,7 @@ module Planter
34
40
  coerced.each { |k, v| Planter.variables[k] = v }
35
41
 
36
42
  # Ask user for any variables not already defined
37
- Planter.config[:variables].each do |var|
43
+ @config.variables.each do |var|
38
44
  key = var[:key].to_var
39
45
  next if Planter.variables.keys.include?(key)
40
46
 
@@ -46,7 +52,8 @@ module Planter
46
52
  value: var[:value],
47
53
  min: var[:min],
48
54
  max: var[:max],
49
- choices: var[:choices] || nil
55
+ choices: var[:choices] || nil,
56
+ date_format: var[:date_format] || nil
50
57
  )
51
58
  answer = q.ask
52
59
  if answer.nil?
@@ -55,7 +62,7 @@ module Planter
55
62
  answer = var[:default]
56
63
  end
57
64
 
58
- Planter.variables[key] = answer
65
+ Planter.variables[key] = answer.apply_all
59
66
  end
60
67
 
61
68
  git_pull if @repo
@@ -94,26 +101,26 @@ module Planter
94
101
  def git_pull
95
102
  Planter.spinner.update(title: 'Pulling git repo')
96
103
 
97
- raise Errors::GitError.new('`git` executable not found') unless TTY::Which.exist?('git')
104
+ die('`git` executable not found', :git) unless TTY::Which.exist?('git')
98
105
 
99
106
  pwd = Dir.pwd
100
107
  @repo = expand_repo(@repo)
101
108
 
102
109
  if File.exist?(repo_dir)
103
110
  Dir.chdir(repo_dir)
104
- raise Errors::GitError.new("Directory #{repo_dir} exists but is not git repo") unless File.exist?('.git')
111
+ die("Directory #{repo_dir} exists but is not git repo", :git) unless File.exist?('.git')
105
112
 
106
113
  res = `git pull`
107
- raise Errors::GitError.new("Error pulling #{@repo}:\n#{res}") unless $?.success?
114
+ die("Error pulling #{@repo}:\n#{res}", :git) unless $?.success?
108
115
  else
109
116
  Dir.chdir(@basedir)
110
117
  res = `git clone "#{@repo}" "#{repo_dir}"`
111
- raise Errors::GitError.new("Error cloning #{@repo}:\n#{res}") unless $?.success?
118
+ die("Error cloning #{@repo}:\n#{res}", :git) unless $?.success?
112
119
  end
113
120
  Dir.chdir(pwd)
114
121
  @basedir = repo_dir
115
122
  rescue StandardError => e
116
- raise Errors::GitError.new("Error pulling #{@repo}:\n#{e.message}")
123
+ die("Error pulling #{@repo}:\n#{e.message}", :git)
117
124
  end
118
125
 
119
126
  ##
@@ -139,7 +146,7 @@ module Planter
139
146
  end
140
147
 
141
148
  if @git
142
- raise Errors::GitError.new('`git` executable not found') unless TTY::Which.exist?('git')
149
+ die('`git` executable not found', :git) unless TTY::Which.exist?('git')
143
150
 
144
151
  Planter.spinner.update(title: 'Initializing git repo')
145
152
  res = add_git
@@ -149,10 +156,10 @@ module Planter
149
156
  end
150
157
  end
151
158
 
152
- if Planter.config[:script]
159
+ if @config.script
153
160
  Planter.spinner.update(title: 'Running script')
154
161
 
155
- scripts = Planter.config[:script]
162
+ scripts = @config.script
156
163
  scripts = [scripts] if scripts.is_a?(String)
157
164
  scripts.each do |script|
158
165
  s = Planter::Script.new(@basedir, Dir.pwd, script)
@@ -184,7 +191,7 @@ module Planter
184
191
 
185
192
  content = IO.read(file)
186
193
 
187
- new_content = content.apply_logic.apply_variables.apply_regexes
194
+ new_content = content.apply_all
188
195
 
189
196
  new_content.gsub!(%r{^.{.4}/?merge *.{,4}\n}, '') if new_content =~ /^.{.4}merge *\n/
190
197
 
@@ -21,9 +21,10 @@ module Planter
21
21
  @min = question[:min]&.to_f || 1.0
22
22
  @max = question[:max]&.to_f || 10.0
23
23
  @prompt = question[:prompt] || nil
24
- @default = question[:default]
24
+ @default = question[:default]&.to_s&.apply_all || nil
25
25
  @value = question[:value]
26
26
  @choices = question[:choices] || []
27
+ @date_format = question[:date_format] || nil
27
28
  @gum = false # TTY::Which.exist?('gum')
28
29
  end
29
30
 
@@ -35,7 +36,7 @@ module Planter
35
36
  def ask
36
37
  return nil if @prompt.nil?
37
38
 
38
- return @value.to_s.apply_variables.apply_regexes.coerce(@type) if @value && @type != :date
39
+ return @value.to_s.apply_all.coerce(@type) if @value && @type != :date
39
40
 
40
41
  res = case @type
41
42
  when :choice
@@ -58,9 +59,9 @@ module Planter
58
59
  read_line
59
60
  end
60
61
  Planter.notify("{dw}#{prompt} => {dy}#{res}{x}", :debug, newline: false)
61
- res
62
+ res.to_s.apply_all
62
63
  rescue TTY::Reader::InputInterrupt
63
- raise Errors::InputError.new('Canceled')
64
+ die('Canceled')
64
65
  end
65
66
 
66
67
  private
@@ -72,8 +73,10 @@ module Planter
72
73
  ##
73
74
  ## @return [Number] numeric response
74
75
  ##
76
+ ## @api private
77
+ ##
75
78
  def read_number(integer: false)
76
- default = @default ? " {bw}[#{@default}]" : ''
79
+ default = @default ? " {xw}[{xbw}#{@default}{xw}]" : ''
77
80
  Planter.notify("{by}#{@prompt} {xc}({bw}#{@min}{xc}-{bw}#{@max}{xc})#{default}")
78
81
 
79
82
  res = @gum ? read_number_gum : read_line_tty
@@ -91,19 +94,26 @@ module Planter
91
94
  ##
92
95
  ## @return [String] date string
93
96
  ##
97
+ ## @api private
98
+ ##
94
99
  def date_default
95
100
  default = @value || @default
96
101
  return nil unless default
97
102
 
103
+ if default =~ /'.*?'/
104
+ @date_format = default.match(/'(.*?)'/)[1].strip
105
+ default = default.gsub(/'.*?'/, '').strip
106
+ end
107
+
98
108
  case default
99
109
  when /^(no|ti)/
100
- Time.now.strftime('%Y-%m-%d %H:%M')
110
+ Time.now.strftime(@date_format || '%Y-%m-%d %H:%M')
101
111
  when /^(to|da)/
102
- Time.now.strftime('%Y-%m-%d')
112
+ Time.now.strftime(@date_format || '%Y-%m-%d')
103
113
  when /^%/
104
- Time.now.strftime(@default)
114
+ Time.now.strftime(default)
105
115
  else
106
- Chronic.parse(default).strftime('%Y-%m-%d')
116
+ Chronic.parse(default).strftime(@date_format || '%Y-%m-%d')
107
117
  end
108
118
  end
109
119
 
@@ -114,6 +124,8 @@ module Planter
114
124
  ##
115
125
  ## @return [Date] Parsed Date object
116
126
  ##
127
+ ## @api private
128
+ ##
117
129
  def read_date(prompt: nil)
118
130
  prompt ||= @prompt
119
131
  default = date_default
@@ -123,7 +135,7 @@ module Planter
123
135
  line = @gum ? read_line_gum : read_line_tty
124
136
  return default unless line
125
137
 
126
- Chronic.parse(line).strftime('%Y-%m-%d')
138
+ Chronic.parse(line).strftime(@date_format || '%Y-%m-%d')
127
139
  end
128
140
 
129
141
  ##
@@ -135,7 +147,11 @@ module Planter
135
147
  ##
136
148
  ## @return [String] the single-line response
137
149
  ##
150
+ ## @api private
151
+ ##
138
152
  def read_line(prompt: nil)
153
+ return @default if Planter.accept_defaults || ENV['PLANTER_DEBUG']
154
+
139
155
  prompt ||= @prompt
140
156
  default = @default ? " {bw}[#{@default}]" : ''
141
157
  Planter.notify("{by}#{prompt}#{default}", newline: false)
@@ -156,6 +172,8 @@ module Planter
156
172
  ##
157
173
  ## @return [String] the multi-line response
158
174
  ##
175
+ ## @api private
176
+ ##
159
177
  def read_lines(prompt: nil)
160
178
  prompt ||= @prompt
161
179
  save = @gum ? 'Ctrl-J for newline, Enter' : 'Ctrl-D'
@@ -172,6 +190,8 @@ module Planter
172
190
  ##
173
191
  ## @return [String] String response
174
192
  ##
193
+ ## @api private
194
+ ##
175
195
  def read_number_gum
176
196
  trap('SIGINT') { exit! }
177
197
  res = `gum input --placeholder "#{@min}-#{@max}"`.strip
@@ -185,6 +205,8 @@ module Planter
185
205
  ##
186
206
  ## @return [String] String response
187
207
  ##
208
+ ## @api private
209
+ ##
188
210
  def read_line_tty
189
211
  trap('SIGINT') { exit! }
190
212
  reader = TTY::Reader.new
@@ -199,6 +221,8 @@ module Planter
199
221
  ##
200
222
  ## @return [String] String response
201
223
  ##
224
+ ## @api private
225
+ ##
202
226
  def read_line_gum
203
227
  trap('SIGINT') { exit! }
204
228
  res = `gum input --placeholder "(blank to use default)"`.strip
@@ -212,6 +236,8 @@ module Planter
212
236
  ##
213
237
  ## @return [string] multiline input
214
238
  ##
239
+ ## @api private
240
+ ##
215
241
  def read_mutliline_tty
216
242
  trap('SIGINT') { exit! }
217
243
  reader = TTY::Reader.new
@@ -224,6 +250,8 @@ module Planter
224
250
  ##
225
251
  ## @return [string] multiline input
226
252
  ##
253
+ ## @api private
254
+ ##
227
255
  def read_multiline_gum(prompt)
228
256
  trap('SIGINT') { exit! }
229
257
  width = TTY::Screen.cols > 80 ? 80 : TTY::Screen.cols
@@ -234,7 +262,7 @@ module Planter
234
262
  ##
235
263
  ## Choose from an array of multiple choices. Letter surrounded in
236
264
  ## parenthesis becomes character for response. Only one letter should be
237
- ## specified and must be unique.
265
+ ## specified per choice and must be unique.
238
266
  ##
239
267
  ## @param choices [Array] The choices
240
268
  ## @param prompt [String] The prompt
@@ -260,6 +288,8 @@ module Planter
260
288
  values = choices.to_values.map(&:clean_value)
261
289
  end
262
290
 
291
+ die('Choice(s) without selector, please edit config') unless keys.all?(&:selector?)
292
+
263
293
  default = case default_response.to_s
264
294
  when /^\d+$/
265
295
  values[default.to_i]
@@ -307,6 +337,8 @@ module Planter
307
337
 
308
338
  res = res.empty? ? default : res
309
339
 
340
+ return choice(choices, prompt, default_response: default_response) if res.nil? || res.empty?
341
+
310
342
  if res.to_i.positive?
311
343
  values[res.to_i - 1]
312
344
  elsif res =~ /^[a-z]/ && keys&.option_index(res)
@@ -14,12 +14,12 @@ module Planter
14
14
  ##
15
15
  def initialize(template_dir, output_dir, script)
16
16
  found = find_script(template_dir, script)
17
- raise ScriptError.new("Script #{script} not found") unless found
17
+ die("Script #{script} not found", :script) unless found
18
18
 
19
19
  @script = found
20
20
  make_executable
21
21
 
22
- raise ScriptError.new("Output directory #{output_dir} not found") unless File.directory?(output_dir)
22
+ die("Output directory #{output_dir} not found", :script) unless File.directory?(output_dir)
23
23
 
24
24
  @template_directory = template_dir
25
25
  @directory = output_dir
@@ -63,7 +63,7 @@ module Planter
63
63
  stdout, stderr, status = Open3.capture3(@script, @template_directory, @directory)
64
64
  Planter.notify("STDOUT:\n#{stdout}", :debug) unless stdout.empty?
65
65
  Planter.notify("STDERR:\n#{stderr}", :debug) unless stderr.empty?
66
- raise ScriptError.new("Error running #{@script}") unless status.success?
66
+ die("Error running #{@script}", :script) unless status.success?
67
67
 
68
68
  true
69
69
  end