planter-cli 0.0.3 → 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.rubocop.yml +5 -7
  4. data/CHANGELOG.md +21 -0
  5. data/README.md +28 -1
  6. data/Rakefile +54 -18
  7. data/bin/plant +6 -0
  8. data/docker/Dockerfile-2.6 +5 -5
  9. data/docker/Dockerfile-2.7 +3 -3
  10. data/docker/Dockerfile-3.0 +3 -3
  11. data/lib/planter/array.rb +51 -0
  12. data/lib/planter/color.rb +1 -1
  13. data/lib/planter/errors.rb +14 -0
  14. data/lib/planter/file.rb +87 -4
  15. data/lib/planter/fileentry.rb +5 -1
  16. data/lib/planter/filelist.rb +43 -7
  17. data/lib/planter/hash.rb +81 -84
  18. data/lib/planter/plant.rb +4 -10
  19. data/lib/planter/prompt.rb +6 -3
  20. data/lib/planter/script.rb +24 -12
  21. data/lib/planter/string.rb +134 -29
  22. data/lib/planter/tag.rb +54 -0
  23. data/lib/planter/version.rb +1 -1
  24. data/lib/planter.rb +60 -34
  25. data/planter-cli.gemspec +1 -0
  26. data/spec/config.yml +2 -0
  27. data/spec/planter/array_spec.rb +28 -0
  28. data/spec/planter/file_entry_spec.rb +40 -0
  29. data/spec/planter/file_spec.rb +19 -0
  30. data/spec/planter/filelist_spec.rb +15 -0
  31. data/spec/planter/hash_spec.rb +110 -0
  32. data/spec/planter/plant_spec.rb +1 -0
  33. data/spec/planter/script_spec.rb +80 -0
  34. data/spec/planter/string_spec.rb +215 -2
  35. data/spec/planter/symbol_spec.rb +23 -0
  36. data/spec/planter.yml +6 -0
  37. data/spec/planter_spec.rb +82 -0
  38. data/spec/scripts/test.sh +3 -0
  39. data/spec/scripts/test_fail.sh +3 -0
  40. data/spec/spec_helper.rb +8 -2
  41. data/spec/templates/test/%%project:snake%%.rtf +10 -0
  42. data/spec/templates/test/Rakefile +6 -0
  43. data/spec/templates/test/_planter.yml +12 -0
  44. data/spec/templates/test/_scripts/test.sh +3 -0
  45. data/spec/templates/test/_scripts/test_fail.sh +3 -0
  46. data/spec/templates/test/test.rb +5 -0
  47. data/spec/test_out/image.png +0 -0
  48. data/spec/test_out/test2.rb +5 -0
  49. data/src/_README.md +28 -1
  50. metadata +57 -2
data/lib/planter.rb CHANGED
@@ -6,6 +6,7 @@ require 'json'
6
6
  require 'yaml'
7
7
  require 'fileutils'
8
8
  require 'open3'
9
+ require 'plist'
9
10
 
10
11
  require 'chronic'
11
12
  require 'tty-reader'
@@ -18,23 +19,26 @@ require_relative 'planter/hash'
18
19
  require_relative 'planter/array'
19
20
  require_relative 'planter/symbol'
20
21
  require_relative 'planter/file'
22
+ require_relative 'planter/tag'
21
23
  require_relative 'planter/color'
22
24
  require_relative 'planter/errors'
23
25
  require_relative 'planter/prompt'
24
26
  require_relative 'planter/string'
25
27
  require_relative 'planter/filelist'
26
28
  require_relative 'planter/fileentry'
29
+ require_relative 'planter/script'
27
30
  require_relative 'planter/plant'
28
31
 
29
32
  # Main Journal module
30
33
  module Planter
31
34
  # Base directory for templates
32
- BASE_DIR = File.expand_path('~/.config/planter/')
33
-
34
35
  class << self
35
36
  include Color
36
37
  include Prompt
37
38
 
39
+ ## Base directory for templates
40
+ attr_writer :base_dir
41
+
38
42
  ## Debug mode
39
43
  attr_accessor :debug
40
44
 
@@ -72,7 +76,9 @@ module Planter
72
76
  def notify(string, notification_type = :info, exit_code: nil)
73
77
  case notification_type
74
78
  when :debug
75
- warn "\n{dw}#{string}{x}".x if @debug
79
+ return false unless @debug
80
+
81
+ warn "\n{dw}#{string}{x}".x
76
82
  when :error
77
83
  warn "{br}#{string}{x}".x
78
84
  when :warn
@@ -82,6 +88,8 @@ module Planter
82
88
  end
83
89
 
84
90
  Process.exit exit_code unless exit_code.nil?
91
+
92
+ true
85
93
  end
86
94
 
87
95
  ##
@@ -97,6 +105,10 @@ module Planter
97
105
  error_mark: '{br}✖{x}'.x)
98
106
  end
99
107
 
108
+ def base_dir
109
+ @base_dir ||= ENV['PLANTER_BASE_DIR'] || File.join(Dir.home, '.config', 'planter')
110
+ end
111
+
100
112
  ##
101
113
  ## Build a configuration from template name
102
114
  ##
@@ -105,26 +117,31 @@ module Planter
105
117
  ## @return [Hash] Configuration object
106
118
  ##
107
119
  def config=(template)
108
- Planter.spinner.update(title: 'Initializing configuration')
109
120
  @template = template
110
121
  Planter.variables ||= {}
111
- FileUtils.mkdir_p(BASE_DIR) unless File.directory?(BASE_DIR)
112
- base_config = File.join(BASE_DIR, 'config.yml')
122
+ FileUtils.mkdir_p(Planter.base_dir) unless File.directory?(Planter.base_dir)
123
+ base_config = File.join(Planter.base_dir, 'planter.yml')
113
124
 
114
- unless File.exist?(base_config)
125
+ if File.exist?(base_config)
126
+ @config = YAML.load(IO.read(base_config)).symbolize_keys
127
+ else
115
128
  default_base_config = {
116
129
  defaults: false,
117
130
  git_init: false,
118
131
  files: { '_planter.yml' => 'ignore' },
119
- color: true
132
+ color: true,
133
+ preserve_tags: true
120
134
  }
121
- File.open(base_config, 'w') { |f| f.puts(YAML.dump(default_base_config.stringify_keys)) }
122
- Planter.notify("New configuration written to #{config}, edit as needed.", :warn)
135
+ begin
136
+ File.open(base_config, 'w') { |f| f.puts(YAML.dump(default_base_config.stringify_keys)) }
137
+ rescue Errno::ENOENT
138
+ Planter.notify("Unable to create #{base_config}", :error)
139
+ end
140
+ @config = default_base_config.symbolize_keys
141
+ Planter.notify("New configuration written to #{base_config}, edit as needed.", :warn)
123
142
  end
124
143
 
125
- @config = YAML.load(IO.read(base_config)).symbolize_keys
126
-
127
- base_dir = File.join(BASE_DIR, 'templates', @template)
144
+ base_dir = File.join(Planter.base_dir, 'templates', @template)
128
145
  unless File.directory?(base_dir)
129
146
  notify("Template #{@template} does not exist", :error)
130
147
  res = Prompt.yn('Create template directory', default_response: false)
@@ -142,13 +159,36 @@ module Planter
142
159
  raise Errors::ConfigError.new "Parse error in configuration file:\n#{e.message}"
143
160
  end
144
161
 
162
+ ##
163
+ ## Execute a shell command and return a Boolean success response
164
+ ##
165
+ ## @param cmd [String] The shell command
166
+ ##
167
+ def pass_fail(cmd)
168
+ _, status = Open3.capture2("#{cmd} &> /dev/null")
169
+ status.exitstatus.zero?
170
+ end
171
+
172
+ ##
173
+ ## Patterns reader, file handling config
174
+ ##
175
+ ## @return [Hash] hash of file patterns
176
+ ##
177
+ def patterns
178
+ @patterns ||= process_patterns
179
+ end
180
+
181
+ private
182
+
145
183
  ##
146
184
  ## Load a template-specific configuration
147
185
  ##
148
186
  ## @return [Hash] updated config object
149
187
  ##
188
+ ## @api private
189
+ ##
150
190
  def load_template_config
151
- base_dir = File.join(BASE_DIR, 'templates', @template)
191
+ base_dir = File.join(Planter.base_dir, 'templates', @template)
152
192
  config = File.join(base_dir, '_planter.yml')
153
193
 
154
194
  unless File.exist?(config)
@@ -165,8 +205,9 @@ module Planter
165
205
  git_init: false,
166
206
  files: { '*.tmp' => 'ignore' }
167
207
  }
208
+ FileUtils.mkdir_p(base_dir)
168
209
  File.open(config, 'w') { |f| f.puts(YAML.dump(default_config.stringify_keys)) }
169
- puts "New configuration written to #{config}, please edit."
210
+ notify("New configuration written to #{config}, please edit.", :warn)
170
211
  Process.exit 0
171
212
  end
172
213
  @config = @config.deep_merge(YAML.load(IO.read(config)).symbolize_keys)
@@ -177,6 +218,8 @@ module Planter
177
218
  ##
178
219
  ## @param key [Symbol] The key in @config to convert
179
220
  ##
221
+ ## @api private
222
+ ##
180
223
  def config_array_to_hash(key)
181
224
  files = {}
182
225
  @config[key].each do |k, v|
@@ -185,20 +228,13 @@ module Planter
185
228
  @config[key] = files
186
229
  end
187
230
 
188
- ##
189
- ## Patterns reader, file handling config
190
- ##
191
- ## @return [Hash] hash of file patterns
192
- ##
193
- def patterns
194
- @patterns ||= process_patterns
195
- end
196
-
197
231
  ##
198
232
  ## Process :files in config into regex pattern/operator pairs
199
233
  ##
200
234
  ## @return [Hash] { regex => operator } hash
201
235
  ##
236
+ ## @api private
237
+ ##
202
238
  def process_patterns
203
239
  patterns = {}
204
240
  @config[:files].each do |file, oper|
@@ -208,15 +244,5 @@ module Planter
208
244
  end
209
245
  patterns
210
246
  end
211
-
212
- ##
213
- ## Execute a shell command and return a Boolean success response
214
- ##
215
- ## @param cmd [String] The shell command
216
- ##
217
- def pass_fail(cmd)
218
- _, status = Open3.capture2("#{cmd} &> /dev/null")
219
- status.exitstatus.zero?
220
- end
221
247
  end
222
248
  end
data/planter-cli.gemspec CHANGED
@@ -41,6 +41,7 @@ Gem::Specification.new do |spec|
41
41
  spec.add_development_dependency 'yard', '~> 0.9.5'
42
42
 
43
43
  spec.add_runtime_dependency 'chronic', '~> 0.10'
44
+ spec.add_runtime_dependency 'plist', '~> 3.7.1'
44
45
  spec.add_runtime_dependency 'tty-reader', '~> 0.9'
45
46
  spec.add_runtime_dependency 'tty-screen', '~> 0.8'
46
47
  spec.add_runtime_dependency 'tty-spinner', '~> 0.9'
data/spec/config.yml ADDED
@@ -0,0 +1,2 @@
1
+ ---
2
+ git_init: false
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ::Array do
6
+ let(:array) { [1, 'value1', 'value2', { key1: 'value1', 'key2' => 'value2' }, %w[key1 key2]] }
7
+
8
+ describe '.stringify_keys' do
9
+ it 'converts string keys to strings' do
10
+ result = array.stringify_keys
11
+ expect(result).to eq([1, 'value1', 'value2', { 'key1' => 'value1', 'key2' => 'value2' }, %w[key1 key2]])
12
+ end
13
+ end
14
+
15
+ describe '.abbr_choices' do
16
+ it 'abbreviates the choices' do
17
+ arr = ['(o)ption 1', '(s)econd option', '(t)hird option']
18
+ result = arr.abbr_choices
19
+ expect(result).to match(%r{{xdw}\[{xbw}o{dw}/{xbw}s{dw}/{xbw}t{dw}\]{x}})
20
+ end
21
+
22
+ it 'handles a default' do
23
+ arr = ['(o)ption 1', '(s)econd option', '(t)hird option']
24
+ result = arr.abbr_choices(default: 'o')
25
+ expect(result).to match(%r{{xdw}\[{xbc}o{dw}/{xbw}s{dw}/{xbw}t{dw}\]{x}})
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Planter::FileEntry do
6
+ subject do
7
+ Planter::FileEntry.new(File.expand_path('spec/templates/test/test.rb'), File.expand_path('spec/test_out/test.rb'),
8
+ :ignore)
9
+ end
10
+
11
+ describe '#initialize' do
12
+ it 'makes a new instance' do
13
+ expect(subject).to be_a described_class
14
+ end
15
+ end
16
+
17
+ describe '#to_s' do
18
+ it 'returns the name of the file' do
19
+ expect(subject.to_s).to be_a(String)
20
+ end
21
+ end
22
+
23
+ describe '#inspect' do
24
+ it 'returns a string representation of the file' do
25
+ expect(subject.inspect).to be_a(String)
26
+ end
27
+ end
28
+
29
+ describe '#ask_operation' do
30
+ it 'returns :copy' do
31
+ expect(subject.ask_operation).to eq(:copy)
32
+ end
33
+
34
+ it 'returns :ignore for existing file' do
35
+ fileentry = Planter::FileEntry.new(File.expand_path('spec/templates/test/test.rb'), File.expand_path('spec/test_out/test2.rb'),
36
+ :ignore)
37
+ expect(fileentry.ask_operation).to eq(:ignore)
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ::File do
6
+ describe '.binary?' do
7
+ it 'detects a non-binary text file' do
8
+ expect(File.binary?('spec/test_out/test2.rb')).to be(false)
9
+ end
10
+
11
+ it 'detects a binary image' do
12
+ expect(File.binary?('spec/test_out/image.png')).to be(true)
13
+ end
14
+
15
+ it 'recognizes json as text' do
16
+ expect(File.binary?('spec/test_out/doing.sublime-project')).to be(false)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Planter::FileList do
6
+ describe '#initialize' do
7
+ it 'initializes with an empty list' do
8
+ Planter.base_dir = File.expand_path('spec')
9
+ Planter.variables = { project: 'Untitled', script: 'Script', title: 'Title' }
10
+ Planter.config = 'test'
11
+ filelist = Planter::FileList.new
12
+ expect(filelist.files).not_to eq([])
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe ::Hash do
6
+ let(:hash) { { 'key1' => 'value1', 'key2' => 'value2' } }
7
+
8
+ describe '.symbolize_keys' do
9
+ it 'converts string keys to symbols' do
10
+ string_hash = { 'key1' => 'value1', 'key2' => 'value2', 'key3' => ['value3'] }
11
+ result = string_hash.symbolize_keys
12
+ expect(result).to eq({ key1: 'value1', key2: 'value2', key3: ['value3'] })
13
+ end
14
+
15
+ it 'handles nested hashes' do
16
+ nested_hash = { 'outer' => { 'inner' => 'value' } }
17
+ result = nested_hash.symbolize_keys
18
+ expect(result).to eq({ outer: { inner: 'value' } })
19
+ end
20
+
21
+ it 'handles empty hashes' do
22
+ result = {}.symbolize_keys
23
+ expect(result).to eq({})
24
+ end
25
+ end
26
+
27
+ describe '.stringify_keys' do
28
+ it 'converts symbol keys to strings' do
29
+ symbol_hash = { key1: 'value1', key2: 'value2', key3: ['value3'] }
30
+ result = symbol_hash.stringify_keys
31
+ expect(result).to eq({ 'key1' => 'value1', 'key2' => 'value2', 'key3' => ['value3'] })
32
+ end
33
+
34
+ it 'handles nested hashes' do
35
+ nested_hash = { outer: { inner: 'value' } }
36
+ result = nested_hash.stringify_keys
37
+ expect(result).to eq({ 'outer' => { 'inner' => 'value' } })
38
+ end
39
+
40
+ it 'handles empty hashes' do
41
+ result = {}.stringify_keys
42
+ expect(result).to eq({})
43
+ end
44
+ end
45
+
46
+ describe '.deep_merge' do
47
+ it 'merges two hashes deeply' do
48
+ hash1 = { a: 1, b: { c: 2 }, f: [1, 2] }
49
+ hash2 = { b: { d: 3 }, e: 4, f: [3, 4] }
50
+ result = hash1.deep_merge(hash2)
51
+ expect(result).to eq({ a: 1, b: { c: 2, d: 3 }, e: 4, f: [1, 2, 3, 4] })
52
+ end
53
+
54
+ it 'handles empty hashes' do
55
+ result = {}.deep_merge({})
56
+ expect(result).to eq({})
57
+ end
58
+
59
+ it 'does not modify the original hashes' do
60
+ hash1 = { a: 1, b: { c: 2 }, f: 'test' }
61
+ hash2 = { b: { d: 3 }, e: 4, f: nil }
62
+ hash1.deep_merge(hash2)
63
+ expect(hash1).to eq({ a: 1, b: { c: 2 }, f: 'test' })
64
+ expect(hash2).to eq({ b: { d: 3 }, e: 4, f: nil })
65
+ end
66
+ end
67
+
68
+ describe '.deep_freeze' do
69
+ it 'freezes all nested hashes' do
70
+ hash = { a: 1, b: { c: 2 } }.deep_freeze
71
+ expect(hash).to be_frozen
72
+ expect(hash[:b]).to be_frozen
73
+ end
74
+ end
75
+
76
+ describe '.deep_thaw' do
77
+ it 'thaws all nested hashes' do
78
+ hash = { a: 1, b: { c: 2 } }.deep_freeze.deep_thaw
79
+ expect(hash).not_to be_frozen
80
+ expect(hash[:b]).not_to be_frozen
81
+ end
82
+ end
83
+
84
+ describe '#stringify_keys!' do
85
+ let(:hash) { { key1: 'value1', key2: 'value2', key3: { key4: 'value4' } } }
86
+
87
+ it 'converts symbol keys to strings destructively' do
88
+ hash.stringify_keys!
89
+ expect(hash).to eq({ 'key1' => 'value1', 'key2' => 'value2', 'key3' => { 'key4' => 'value4' } })
90
+ end
91
+
92
+ it 'does not modify the original hash if already string keys' do
93
+ string_hash = { 'key1' => 'value1', 'key2' => 'value2', 'key3' => { 'key4' => 'value4' } }
94
+ string_hash.stringify_keys!
95
+ expect(string_hash).to eq({ 'key1' => 'value1', 'key2' => 'value2', 'key3' => { 'key4' => 'value4' } })
96
+ end
97
+
98
+ it 'handles nested hashes' do
99
+ nested_hash = { key1: { key2: { key3: 'value3' } } }
100
+ nested_hash.stringify_keys!
101
+ expect(nested_hash).to eq({ 'key1' => { 'key2' => { 'key3' => 'value3' } } })
102
+ end
103
+
104
+ it 'handles empty hashes' do
105
+ empty_hash = {}
106
+ empty_hash.stringify_keys!
107
+ expect(empty_hash).to eq({})
108
+ end
109
+ end
110
+ end
@@ -4,6 +4,7 @@ require 'spec_helper'
4
4
 
5
5
  describe Planter::Plant do
6
6
  Planter.accept_defaults = true
7
+ Planter.base_dir = File.expand_path('spec')
7
8
  subject(:ruby_gem) { Planter::Plant.new('test', { project: 'Untitled', script: 'Script', title: 'Title' }) }
8
9
 
9
10
  describe '.new' do
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'fileutils'
5
+
6
+ describe Planter::Script do
7
+ let(:template_dir) { File.expand_path('spec/templates/test') }
8
+ let(:output_dir) { File.expand_path('spec/test_out') }
9
+ let(:script_name) { 'test.sh' }
10
+ let(:script_name_fail) { 'test_fail.sh' }
11
+ let(:script_path) { File.join(template_dir, '_scripts', script_name) }
12
+ let(:base_script_path) { File.join(Planter.base_dir, 'scripts', script_name) }
13
+
14
+ before do
15
+ Planter.base_dir = File.expand_path('spec')
16
+ allow(File).to receive(:exist?).and_call_original
17
+ allow(File).to receive(:directory?).and_call_original
18
+ allow(File).to receive(:exist?).with(script_path).and_return(true)
19
+ allow(File).to receive(:exist?).with(base_script_path).and_return(false)
20
+ allow(File).to receive(:directory?).with(output_dir).and_return(true)
21
+ end
22
+
23
+ describe '#initialize' do
24
+ it 'initializes with valid script and directories' do
25
+ script = Planter::Script.new(template_dir, output_dir, script_name)
26
+ expect(script.script).to eq(script_path)
27
+ end
28
+
29
+ it 'raises an error if script is not found' do
30
+ allow(File).to receive(:exist?).with(script_path).and_return(false)
31
+ expect do
32
+ Planter::Script.new(template_dir, output_dir, script_name)
33
+ end.to raise_error(ScriptError)
34
+ end
35
+
36
+ it 'raises an error if output directory is not found' do
37
+ allow(File).to receive(:directory?).with(output_dir).and_return(false)
38
+ expect do
39
+ Planter::Script.new(template_dir, output_dir, script_name)
40
+ end.to raise_error(ScriptError)
41
+ end
42
+ end
43
+
44
+ describe '#find_script' do
45
+ it 'finds the script in the template directory' do
46
+ script = Planter::Script.new(template_dir, output_dir, script_name)
47
+ expect(script.find_script(template_dir, script_name)).to eq(script_path)
48
+ end
49
+
50
+ it 'finds the script in the base directory' do
51
+ allow(File).to receive(:exist?).with(script_path).and_return(false)
52
+ allow(File).to receive(:exist?).with(base_script_path).and_return(true)
53
+ script = Planter::Script.new(template_dir, output_dir, script_name)
54
+ expect(script.find_script(template_dir, script_name)).to eq(base_script_path)
55
+ end
56
+
57
+ it 'returns nil if script is not found' do
58
+ allow(File).to receive(:exist?).with(script_path).and_return(false)
59
+ allow(File).to receive(:exist?).with(base_script_path).and_return(false)
60
+ expect do
61
+ script = Planter::Script.new(template_dir, output_dir, script_name)
62
+ script.find_script(template_dir, script_name)
63
+ end.to raise_error(ScriptError)
64
+ end
65
+ end
66
+
67
+ describe '#run' do
68
+ it 'executes the script successfully' do
69
+ script = Planter::Script.new(template_dir, output_dir, script_name)
70
+ expect(script.run).to be true
71
+ end
72
+
73
+ it 'raises an error if script execution fails' do
74
+ script = Planter::Script.new(template_dir, output_dir, script_name_fail)
75
+ expect do
76
+ script.run
77
+ end.to raise_error(ScriptError)
78
+ end
79
+ end
80
+ end