envoy-cli 1.0.0rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +95 -0
  5. data/.ruby-version +1 -0
  6. data/Gemfile +4 -0
  7. data/bin/envoy +12 -0
  8. data/bin/envoy.bak +485 -0
  9. data/bootstrap/base/.envoyrc +1 -0
  10. data/bootstrap/base/.jshintrc +91 -0
  11. data/bootstrap/base/.nvmrc +1 -0
  12. data/bootstrap/base/config/.gitkeep +0 -0
  13. data/bootstrap/base/docs/.gitkeep +0 -0
  14. data/bootstrap/base/gulpfile.js +0 -0
  15. data/bootstrap/base/i18n/.gitkeep +0 -0
  16. data/bootstrap/base/index.js +5 -0
  17. data/bootstrap/base/lib/.gitkeep +0 -0
  18. data/bootstrap/base/routes/.gitkeep +0 -0
  19. data/bootstrap/base/views/.gitkeep +0 -0
  20. data/bootstrap/base/workers/.gitkeep +0 -0
  21. data/bootstrap/events/generic.json +6 -0
  22. data/bootstrap/events/host_notification.json +27 -0
  23. data/bootstrap/events/route.json +6 -0
  24. data/bootstrap/events/sms_host_notification.json +158 -0
  25. data/bootstrap/templates/docs/about.md.erb +3 -0
  26. data/bootstrap/templates/gitignore.erb +8 -0
  27. data/bootstrap/templates/index.js.erb +5 -0
  28. data/bootstrap/templates/license.md.erb +1 -0
  29. data/bootstrap/templates/npmignore.erb +0 -0
  30. data/bootstrap/templates/package.json +24 -0
  31. data/bootstrap/templates/readme.md.erb +9 -0
  32. data/bootstrap/templates/routes/callback.js.erb +10 -0
  33. data/bootstrap/templates/routes/html.js.erb +9 -0
  34. data/bootstrap/templates/routes/json.js.erb +9 -0
  35. data/bootstrap/templates/routes/other.js.erb +9 -0
  36. data/bootstrap/templates/views/hello.html.erb +91 -0
  37. data/bootstrap/templates/views/starter.html.erb +10 -0
  38. data/bootstrap/templates/worker.js.erb +10 -0
  39. data/envoy-cli.gemspec +29 -0
  40. data/lib/envoy.rb +70 -0
  41. data/lib/envoy/version.rb +3 -0
  42. data/lib/inc/commands.rb +74 -0
  43. data/lib/inc/generator.rb +159 -0
  44. data/lib/inc/mixins.rb +347 -0
  45. data/lib/inc/runner.rb +21 -0
  46. data/lib/tasks/plugin.rb +181 -0
  47. data/lib/tasks/routes.rb +39 -0
  48. data/lib/tasks/test.rb +68 -0
  49. data/lib/tasks/workers.rb +39 -0
  50. data/readme.md +0 -0
  51. metadata +276 -0
@@ -0,0 +1,10 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Hello World</title>
6
+ </head>
7
+ <body>
8
+ Hello World
9
+ </body>
10
+ </html>
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Handle the <%=@job_name%> job
3
+ *
4
+ * @param EnvoyRequest req
5
+ * @param EnvoyResponse res
6
+ */
7
+ module.exports = function(req, res) {
8
+ var data = {};
9
+ res.job_complete('status message', data);
10
+ }
data/envoy-cli.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ $LOAD_PATH.push File.expand_path("../lib", __FILE__)
2
+ require 'envoy/version'
3
+ Gem::Specification.new do |spec|
4
+ spec.name = 'envoy-cli'
5
+ spec.version = Envoy::VERSION
6
+ spec.authors = ["David Boskovic"]
7
+ spec.email = ["david@envoy.com"]
8
+
9
+ spec.date = '2016-03-18'
10
+ spec.summary = 'Envoy platform command line interface.'
11
+ spec.description = 'The Envoy command line interface handles almost all the heavy lifting for you during '\
12
+ 'development of platform plugins.'
13
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
14
+ spec.homepage = 'http://rubygemspec.org/gems/envoy-cli'
15
+ spec.license = 'MIT'
16
+
17
+ spec.add_runtime_dependency "thor", "~> 0.19", ">= 0.19.1"
18
+ spec.add_runtime_dependency "terminal-table", "~> 1.5", ">= 1.5.2"
19
+ spec.add_runtime_dependency "colorize", "~> 0.7", ">= 0.7.7"
20
+ spec.add_runtime_dependency "unirest", "~> 1.1", ">= 1.1.2"
21
+ spec.add_runtime_dependency "parseconfig", "~> 1.0", ">= 1.0.8"
22
+ spec.add_runtime_dependency "coderay", "~> 1.1", ">= 1.1.1"
23
+ spec.add_runtime_dependency "launchy", "~> 2.4", ">= 2.4.3"
24
+ spec.add_runtime_dependency "tty", "~> 0.5", ">= 0.5.0"
25
+ spec.add_runtime_dependency "net-http-uploadprogress", "~> 2.0", ">= 2.0.0"
26
+
27
+ spec.bindir = 'bin'
28
+ spec.executables << 'envoy'
29
+ end
data/lib/envoy.rb ADDED
@@ -0,0 +1,70 @@
1
+ require 'envoy/version'
2
+ require 'thor'
3
+
4
+ require 'inc/runner'
5
+ require 'inc/commands'
6
+ require 'inc/mixins'
7
+ require 'inc/generator'
8
+
9
+ require 'unirest'
10
+ require 'colorize'
11
+
12
+ project_root = File.dirname(File.absolute_path(__FILE__))
13
+ Dir.glob("#{project_root}/tasks/**/*.rb", &method(:require))
14
+
15
+ module Envoy
16
+ class Main < Commands
17
+ include Mixins
18
+
19
+ namespace :envoy
20
+
21
+ # Authenticates the user by storing their auth_token in ~/.envoy-cfg
22
+ # @param --local [String] will use localhost:3000 as the API url
23
+ # @param --path [String] can be used to override the endpoint for this profile
24
+ # @param --profile NAME [String] will save the login information for that profile, in order
25
+ # to use that info, you must specify the same profile name on other requests
26
+
27
+ desc "login", "Login to your account"
28
+ option :path, type: :string
29
+
30
+ def login
31
+ # collect login information
32
+ email = prompt.ask "Email:"
33
+ password = prompt.mask "Password:"
34
+
35
+ # attempt to obtain a working token
36
+ res = post('Authenticating', base_url!('oauth/token'), {
37
+ grant_type: 'password',
38
+ username: email,
39
+ password: password
40
+ })
41
+
42
+ # make sure the user has a developer account
43
+ post('Fetching developer profile', base_url!('./platform/developers'), {
44
+ access_token: res['access_token']
45
+ })
46
+
47
+ # save user to config
48
+ if options.profile && !config.groups.include?(options.profile)
49
+ config.groups.push options.profile
50
+ end
51
+
52
+ # setup data to save
53
+ conf = {
54
+ 'email' => email,
55
+ 'access_token' => res['access_token']
56
+ }
57
+ conf['local'] = true if options.local
58
+ conf['api_path'] = options.path if options.path
59
+
60
+ config.params[profile(options)] = conf
61
+
62
+ write_config!
63
+
64
+ say_status "Success", "You are now logged in.", :green
65
+ end
66
+ end
67
+ end
68
+
69
+ Runner.setup Envoy
70
+ Runner.start
@@ -0,0 +1,3 @@
1
+ module Envoy
2
+ VERSION = "1.0.0rc1".freeze
3
+ end
@@ -0,0 +1,74 @@
1
+
2
+ require 'thor'
3
+ class Commands < Thor
4
+ include Thor::Actions
5
+
6
+ class_option :profile, type: :string
7
+ class_option :local, type: :boolean
8
+ class_option :debug, type: :boolean
9
+
10
+ class << self
11
+ def source_root
12
+ File.expand_path('../../bootstrap', File.dirname(__FILE__))
13
+ end
14
+
15
+ def setup(mod)
16
+ @mod = mod
17
+ end
18
+
19
+ # Override Thor#help so it can give information about any class and any method.
20
+ #
21
+ def help(shell, subcommand = false)
22
+ list = printable_commands(true, subcommand)
23
+ # puts Thor::Base.subclasses
24
+ thor_classes_in(@mod).each do |klass|
25
+ # puts klass.name
26
+ list += klass.printable_commands(false)
27
+ end
28
+ list.map! do |x|
29
+ x[0].sub!(basenamespace, '')
30
+ x
31
+ end
32
+ list.sort! do |a, b|
33
+ if a[0].include?(':') == false && b[0].include?(':') == true
34
+ out = -1
35
+ elsif a[0].include?(':') == true && b[0].include?(':') == false
36
+ out = +1
37
+ else
38
+ out = a[0] <=> b[0]
39
+ end
40
+ out
41
+ end
42
+
43
+ if defined?(@package_name) && @package_name
44
+ shell.say "#{@package_name} commands:"
45
+ else
46
+ shell.say "Commands:"
47
+ end
48
+
49
+ shell.print_table(list, indent: 2, truncate: true)
50
+ shell.say
51
+ class_options_help(shell)
52
+ end
53
+
54
+ def thor_classes_in(klass)
55
+ stringfied_constants = klass.constants.map &:to_s
56
+ out = Thor::Base.subclasses.select do |subclass|
57
+ next unless subclass.name
58
+ !stringfied_constants.select do |const|
59
+ subclass.name.gsub("#{klass.name}::", "").start_with?(const)
60
+ end.empty?
61
+ end
62
+ # puts out.inspect
63
+ out
64
+ end
65
+
66
+ def basenamespace
67
+ "#{basename}:"
68
+ end
69
+
70
+ def banner(command, _namespace = nil, subcommand = false)
71
+ "#{basename} #{command.formatted_usage(self, $thor_runner, subcommand).sub!(basenamespace, '')}"
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,159 @@
1
+ require 'yaml'
2
+ module Envoy
3
+ module Generator
4
+ def setup_directories
5
+ directory 'base', '.'
6
+ end
7
+
8
+ def setup_config
9
+ create_file 'config/default.json' do
10
+ JSON.pretty_generate({
11
+ 'permissions' => nil,
12
+ 'installable_on' => ['location'],
13
+ 'oauth2' => {},
14
+ 'schema' => {},
15
+ 'setup' => [],
16
+ 'routes' => [],
17
+ 'jobs_handled' => {}
18
+ })
19
+ end
20
+ end
21
+
22
+ def setup_docs
23
+ template 'templates/docs/about.md.erb', 'docs/about.md'
24
+ end
25
+
26
+ def setup_i18n
27
+ create_file 'i18n/en.yaml' do
28
+ {
29
+ 'welcome' => 'Welcome'
30
+ }.to_yaml
31
+ end
32
+ end
33
+
34
+ def setup_route(name, cfg = {})
35
+ @route_name = name
36
+ cfg[:type] ||= prompt.select("Choose route type:", ["html", "json", "callback", "other"])
37
+ template "templates/routes/#{cfg[:type]}.js.erb", "routes/#{name}.js"
38
+ if cfg[:type] == 'html'
39
+ cfg[:view] ||= 'starter'
40
+ template "templates/views/#{cfg[:view]}.html.erb", "views/#{name}.html"
41
+ end
42
+ # get profiles, if more than one show select with default
43
+ profiles = Dir.entries("#{destination_root}/config")
44
+ .select { |file| file.end_with? '.json' }
45
+ .map { |file| file.chomp('.json') }
46
+ if profiles.length > 1
47
+ profiles = prompt.multi_select("Select profiles to add route to:", profiles)
48
+ end
49
+ profiles.each do |profile|
50
+ data = read_data("config/#{profile}.json")
51
+ index = data['routes'].index { |x| x['handler'] == name }
52
+ route_data = {
53
+ "path" => name,
54
+ "handler" => name
55
+ }
56
+ if index
57
+ data['routes'][index] = route_data
58
+ else
59
+ data['routes'].push route_data
60
+ end
61
+ write_data("config/#{profile}.json", data)
62
+ end
63
+ end
64
+
65
+ def teardown_route(name)
66
+ profiles = Dir.entries("#{destination_root}/config")
67
+ .select { |file| file.end_with? '.json' }
68
+ .map { |file| file.chomp('.json') }
69
+ if File.exist? "#{destination_root}/routes/#{name}.js"
70
+ remove_file "routes/#{name}.js"
71
+ else
72
+ say_status :info, "No route handler found for #{name}", :yellow
73
+ end
74
+ if File.exist? "#{destination_root}/views/#{name}.html"
75
+ remove_file "views/#{name}.html"
76
+ end
77
+ profiles.each do |profile|
78
+ data = read_data("config/#{profile}.json")
79
+ index = data['routes'].index { |x| x['handler'] == name }
80
+ if index
81
+ data['routes'].delete_at index
82
+ write_data("config/#{profile}.json", data)
83
+ else
84
+ say_status :info, "Route not in profile: #{profile}", :yellow
85
+ end
86
+ end
87
+ end
88
+
89
+ def get_profiles(choose = true)
90
+ profiles = Dir.entries("#{destination_root}/config")
91
+ .select { |file| file.end_with? '.json' }
92
+ .map { |file| file.chomp('.json') }
93
+ if choose && profiles.length > 1
94
+ profiles = prompt.multi_select("Select profiles to add configuration to:", profiles)
95
+ end
96
+ profiles
97
+ end
98
+
99
+ def setup_worker(name)
100
+ @job_name = name
101
+ template "templates/worker.js.erb", "workers/#{name}.js"
102
+ profiles = get_profiles
103
+ profiles.each do |profile|
104
+ data = read_data("config/#{profile}.json")
105
+ unless data['jobs_handled'][name]
106
+ data['jobs_handled'][name] = {}
107
+ end
108
+ write_data("config/#{profile}.json", data)
109
+ end
110
+ end
111
+
112
+ def teardown_worker(name)
113
+ profiles = Dir.entries("#{destination_root}/config")
114
+ .select { |file| file.end_with? '.json' }
115
+ .map { |file| file.chomp('.json') }
116
+ if File.exist? "#{destination_root}/workers/#{name}.js"
117
+ remove_file "workers/#{name}.js"
118
+ else
119
+ say_status :info, "No worker found for #{name}", :yellow
120
+ end
121
+ profiles.each do |profile|
122
+ data = read_data("config/#{profile}.json")
123
+ if data['jobs_handled'][name]
124
+ data['jobs_handled'].delete name
125
+ write_data("config/#{profile}.json", data)
126
+ else
127
+ say_status :info, "Worker not in profile: #{profile}", :yellow
128
+ end
129
+ end
130
+ end
131
+
132
+ def setup_license
133
+ template 'templates/license.md.erb', 'license.md'
134
+ end
135
+
136
+ def setup_dotfiles
137
+ template 'templates/gitignore.erb', '.gitignore'
138
+ template 'templates/npmignore.erb', '.npmignore'
139
+ end
140
+
141
+ def setup_npm
142
+ copy_file 'templates/package.json', 'package.json', force: true
143
+ data = read_data 'package.json'
144
+ data['name'] = @key
145
+ data['description'] = @description
146
+ data['version'] = @version
147
+ write_data 'package.json', data
148
+ end
149
+
150
+ def setup_readme
151
+ template 'templates/readme.md.erb', 'readme.md'
152
+ end
153
+
154
+ def setup_tests
155
+ say_status '@todo', 'bootstrap tests', :yellow
156
+ # template 'templates/tests/readme.md.erb', 'readme.md'
157
+ end
158
+ end
159
+ end
data/lib/inc/mixins.rb ADDED
@@ -0,0 +1,347 @@
1
+ require 'tty-prompt'
2
+ require 'parseconfig'
3
+ require 'json'
4
+ require 'coderay'
5
+ require 'unirest'
6
+ require 'yaml'
7
+ require "stringio"
8
+
9
+ module Envoy
10
+ module Mixins
11
+ def post!(path, body = {}, headers = {})
12
+ debug('Start', "POST #{base_url(path)}")
13
+ debug('Request', CodeRay.scan(JSON.pretty_generate(body.is_a?(Hash) && body || JSON.parse(body)), :json).terminal)
14
+ res = Unirest.post(base_url(path), parameters: body, headers: headers)
15
+ debug("End", res.code.to_s)
16
+ if res.body.instance_of?(Hash)
17
+ debug("Body", CodeRay.scan(JSON.pretty_generate(res.body), :json).terminal)
18
+ else
19
+ debug("Body", res.body.to_s)
20
+ end
21
+ res
22
+ rescue => e
23
+ # puts e.message
24
+ error(e.message)
25
+ exit
26
+ end
27
+
28
+ def post(info, *args)
29
+ spin("Network: #{info}")
30
+ res = post!(*args)
31
+ if res.code.between?(200, 299)
32
+ spin_success
33
+ else
34
+ handle_errors(res)
35
+ end
36
+ res.body
37
+ end
38
+
39
+ def jsonapi_post(info, path, body = {}, headers = {})
40
+ headers[:'Content-Type'] = 'application/json'
41
+ headers[:Authorization] = "Bearer #{config[profile]['access_token']}"
42
+ post(info, path, { data: body }.to_json, headers)
43
+ end
44
+
45
+ def error(msg)
46
+ if spinner
47
+ spin_error msg
48
+ else
49
+ say_status 'Error', msg, :red
50
+ end
51
+ end
52
+
53
+ def handle_errors(res)
54
+ if res.body.instance_of?(Hash) && (res.body['errors'].nil? || res.body['errors'].empty?)
55
+ if res.body.dig('meta', 'message')
56
+ error(res.body.dig('meta', 'message'))
57
+ elsif res.body.dig('error_description')
58
+ error(res.body.dig('error_description'))
59
+ else
60
+ error('Unkown error occured')
61
+ say CodeRay.scan(JSON.pretty_generate(res.body), :json).terminal
62
+ end
63
+ exit
64
+ elsif res.body.instance_of? String
65
+ error 'Unkown error occured'
66
+ if agree("View full response body? (y/n)")
67
+ ask_editor res.body
68
+ end
69
+ elsif res.body.instance_of?(Hash)
70
+ res.body['errors'].each do |err|
71
+ error err['detail']
72
+ end
73
+ else
74
+ error 'Unkown error occured'
75
+ say res.body
76
+ end
77
+ spin_error
78
+ exit
79
+ end
80
+
81
+ def base_url!(path)
82
+ base_url(path, true)
83
+ end
84
+
85
+ def base_url(path, ignore_profile = false)
86
+ return path if path.start_with?('http://', 'https://')
87
+ local = false
88
+ cfg = config[profile]
89
+ if !ignore_profile && cfg['local']
90
+ local = true
91
+ end
92
+ if options.local
93
+ local = true
94
+ end
95
+ if local
96
+ if cfg && cfg['api_path_local']
97
+ base = cfg['api_path_local']
98
+ else
99
+ base = 'http://localhost:3000'
100
+ end
101
+ elsif ignore_profile && options.path
102
+ base = options.path
103
+ elsif cfg && cfg['api_path']
104
+ base = cfg['api_path']
105
+ else
106
+ base = 'https://app.envoy.com'
107
+ end
108
+ if path.start_with?('./')
109
+ path = 'api/v2/' + path[2..-1]
110
+ end
111
+ if path.start_with?('/')
112
+ path = path[1..-1]
113
+ end
114
+ if options.trace
115
+ debug 'URL', "#{base}/#{path}"
116
+ end
117
+ "#{base}/#{path}"
118
+ end
119
+
120
+ def debug(df, text, type = nil)
121
+ return unless options.trace || options.debug
122
+ say_status df, text, type
123
+ end
124
+
125
+ def debug_val(val, label)
126
+ debug label, val
127
+ val
128
+ end
129
+
130
+ def plugin_uuid
131
+ uuid = local_config["ENVOY_#{profile.upcase}_PLUGIN_UUID"]
132
+ unless uuid
133
+ error "No ENVOY_#{profile.upcase}_PLUGIN_UUID configuration"
134
+ exit
135
+ end
136
+ uuid
137
+ end
138
+
139
+ def profile(_ = false)
140
+ return debug_val(options.profile, 'Profile') if options.profile
141
+ cf = local_config
142
+ return debug_val(cf['ENVOY_PROFILE'], 'Profile') if cf['ENVOY_PROFILE']
143
+ debug_val('default', 'Profile')
144
+ end
145
+
146
+ def manifest
147
+ @manifest ||= JSON.parse(File.read(manifest_file)) || {}
148
+ end
149
+
150
+ def manifest_file(_ = false)
151
+ file = Dir.pwd + '/config/' + profile.downcase + '.json'
152
+ unless File.exist? file
153
+ file = Dir.pwd + '/config/default.json'
154
+ unless File.exist? file
155
+ error "Must have config/default.json or config/" + profile.downcase + '.json'
156
+ exit
157
+ end
158
+ end
159
+ debug_val file, 'Manifest'
160
+ end
161
+
162
+ def local_config
163
+ return @local_config if @local_config
164
+ unless File.exist? Dir.pwd + '/.envoyrc'
165
+ FileUtils.touch(Dir.pwd + '/.envoyrc')
166
+ end
167
+ @local_config = ParseConfig.new Dir.pwd + '/.envoyrc'
168
+ end
169
+
170
+ def print_event(event)
171
+ say "------------------------------------------------------------------------"
172
+ data = event['attributes']
173
+ say "Event UUID => #{event['id']}".green
174
+ say "Timestamp =>".yellow + " #{data['created-at']}"
175
+ say "Task Time =>".yellow + " #{data['task-time']}ms"
176
+ say "Execution Time =>".yellow + " #{data['process-time']}ms"
177
+ say "\n"
178
+ say "REQUEST META"
179
+ req_h = JSON.pretty_generate Envoy.min_json(data['request-meta'] || {})
180
+ say CodeRay.scan(req_h, :json).terminal(line_numbers: :table)
181
+ say "\n"
182
+ say "REQUEST BODY"
183
+ req_h = JSON.pretty_generate Envoy.min_json(data['request-body'] || {})
184
+ say CodeRay.scan(req_h, :json).terminal(line_numbers: :table)
185
+ say "\n"
186
+ say "REPONSE META"
187
+ req_h = JSON.pretty_generate Envoy.min_json(data['response-meta'] || {})
188
+ say CodeRay.scan(req_h, :json).terminal(line_numbers: :table)
189
+ say "\n"
190
+ say "RESPONSE BODY"
191
+ req_h = JSON.pretty_generate Envoy.min_json(data['response-body'] || {})
192
+ say CodeRay.scan(req_h, :json).terminal(line_numbers: :table)
193
+ say "\n"
194
+ say "LOG ENTRIES"
195
+ say data['tail']
196
+ # say data
197
+ end
198
+
199
+ def prompt
200
+ @prompt ||= TTY::Prompt.new
201
+ end
202
+
203
+ def config
204
+ @config ||= config!
205
+ end
206
+
207
+ def write_config!
208
+ file = File.open(Dir.home + '/.envoy-cfg', 'w')
209
+ config.write(file)
210
+ file.close
211
+ end
212
+
213
+ def write_local_config!
214
+ file = File.open(Dir.pwd + '/.envoyrc', 'w')
215
+ local_config.write(file)
216
+ file.close
217
+ end
218
+
219
+ def config!
220
+ if !File.exist?(Dir.home + '/.envoy-cfg')
221
+ file = File.open(Dir.home + '/.envoy-cfg', 'w')
222
+ cfg = ParseConfig.new
223
+ cfg.groups = ['default']
224
+ cfg.params = { 'default' => { 'access_token' => '', 'email' => '' } }
225
+ cfg.write(file)
226
+ file.close
227
+ else
228
+ cfg = ParseConfig.new Dir.home + '/.envoy-cfg'
229
+ end
230
+ cfg
231
+ end
232
+
233
+ def min_json(h)
234
+ out = {}
235
+ h.each do |key, val|
236
+ if val.instance_of?(String) && val.length > 500
237
+ val = '[long string, view specific record for full text]'
238
+ end
239
+ if val.instance_of?(Hash) && val.length > 20
240
+ if val['id']
241
+ out[key] = { id: val['id'], "[#{val.length} keys]": "..." }
242
+ else
243
+ out[key] = "{...#{val.length} keys}"
244
+ end
245
+ elsif val.instance_of?(Hash)
246
+ out[key] = min_json val
247
+ else
248
+ out[key] = val
249
+ end
250
+ end
251
+ out
252
+ end
253
+
254
+ attr_reader :spinner
255
+
256
+ def spin(task)
257
+ return if options.debug
258
+ if @spinner
259
+ @spinner.stop
260
+ end
261
+ spinner = TTY::Spinner.new(":spinner #{task}...", format: :spin_2, interval: 10)
262
+ spinner.start
263
+ @spinner = spinner
264
+ end
265
+
266
+ def spin_success(msg = nil)
267
+ return if options.debug
268
+ return unless @spinner
269
+ @spinner.success msg ? '> ' + msg : ''
270
+ @spinner = nil
271
+ end
272
+
273
+ def spin_error(msg = nil)
274
+ return if options.debug
275
+ return unless @spinner
276
+ @spinner.error msg ? '> ' + msg : ''
277
+ @spinner = nil
278
+ end
279
+
280
+ def read_data(file)
281
+ rfile = file
282
+ unless file.start_with? '/'
283
+ rfile = "#{destination_root}/#{file}"
284
+ end
285
+ unless File.exist? rfile
286
+ return {}
287
+ end
288
+ src = File.read rfile
289
+ return JSON.parse(src) if file.end_with? '.json'
290
+ return YAML.load(src) if file.end_with? '.yaml'
291
+ raise "Can only read data from json or yaml files."
292
+ end
293
+
294
+ def write_data(file, data)
295
+ rfile = file
296
+ unless file.start_with? '/'
297
+ rfile = "#{destination_root}/#{file}"
298
+ end
299
+ if file.end_with? '.json'
300
+ out = JSON.pretty_generate(data)
301
+ elsif file.end_with? '.yaml'
302
+ out = YAML.dump(data)
303
+ else
304
+ raise "Can only write data from json or yaml files."
305
+ end
306
+ File.write(rfile, out)
307
+ say_status :updated, file, :green
308
+ end
309
+
310
+ def indent(depth = 7)
311
+ start_indent depth
312
+ yield
313
+ ensure
314
+ stop_indent
315
+ end
316
+
317
+ def file_exist?(file)
318
+ File.exist?(Dir.pwd + '/' + file)
319
+ end
320
+
321
+ def start_indent(depth = 7)
322
+ $real_stdout = $stdout
323
+ $stdout_depth = depth
324
+ $stdout = StringIO.new
325
+
326
+ closure = lambda do |*args|
327
+ args.each do |line|
328
+ $real_stdout.puts((" " * $stdout_depth) + line)
329
+ end
330
+ end
331
+ $stdout.define_singleton_method(:print, closure)
332
+ $stdout.define_singleton_method(:puts, closure)
333
+ end
334
+
335
+ def stop_indent
336
+ $stdout = $real_stdout
337
+ end
338
+
339
+ def pretty_json(body)
340
+ CodeRay.scan(JSON.pretty_generate(body.is_a?(Hash) && body || JSON.parse(body)), :json).terminal
341
+ end
342
+
343
+ def indent_lines(str, pad = 7)
344
+ str.each_line.map { |l| (' ' * pad) + l }.join
345
+ end
346
+ end
347
+ end