planter-cli 0.0.3 → 3.0.1

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.
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