envoy-cli 1.0.0rc1

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