haplo 2.1.0-java

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.
@@ -0,0 +1,28 @@
1
+
2
+ module PluginTool
3
+
4
+ module LocalConfig
5
+
6
+ LOCAL_CONFIG_FILENAME = "server.config.json"
7
+
8
+ def self.load
9
+ if File.exist? LOCAL_CONFIG_FILENAME
10
+ @@local_config = JSON.parse(File.open(LOCAL_CONFIG_FILENAME) { |f| f.read })
11
+ else
12
+ @@local_config = {}
13
+ end
14
+ end
15
+
16
+ def self.get_list(list_name)
17
+ lookup = @@local_config[list_name]
18
+ return [] unless lookup
19
+ list = []
20
+ server_name = PluginTool.get_server_hostname
21
+ list.concat(lookup['*']) if lookup.has_key?('*')
22
+ list.concat(lookup[server_name]) if server_name && lookup.has_key?(server_name)
23
+ list
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,44 @@
1
+
2
+ module PluginTool
3
+
4
+ # NOTE: Also update packing.rb if this changes
5
+ ALLOWED_PLUGIN_DIRS = ['js', 'static', 'template', 'test', 'data']
6
+
7
+ def self.generate_manifest(directory)
8
+ manifest = Hash.new
9
+ Dir.glob("#{directory}/**/*").sort.each do |pathname|
10
+ # Ignore directories
11
+ next unless File.file? pathname
12
+ # Ignore Emacs backup files
13
+ next if pathname =~ /\~\z/
14
+ # Check file
15
+ filename = pathname.slice(directory.length + 1, pathname.length)
16
+ raise "Bad filename for #{filename}" unless filename =~ /\A([a-zA-Z0-9_\/\-]+\/)?([a-z0-9_-]+\.[a-z0-9]+)\z/
17
+ dir = $1
18
+ name = $2
19
+ if dir != nil
20
+ dir = dir.gsub(/\/\z/,'')
21
+ raise "Bad directory #{dir}" unless dir =~ /\A([a-zA-Z0-9_\-]+)[a-zA-Z0-9_\/\-]*\z/
22
+ raise "Bad root directory #{$1}" unless ALLOWED_PLUGIN_DIRS.include?($1)
23
+ end
24
+ # Get hash of file
25
+ digest = File.open(pathname) { |f| Digest::SHA256.hexdigest(f.read) }
26
+ # And add to manifest
27
+ manifest[filename] = digest
28
+ end
29
+ manifest
30
+ end
31
+
32
+ def self.determine_manifest_changes(from, to)
33
+ changes = []
34
+ from.each_key do |name|
35
+ changes << [name, :delete] unless to.has_key?(name)
36
+ end
37
+ to.each do |name,hash|
38
+ changes << [name, hash] unless from[name] == hash
39
+ end
40
+ changes
41
+ end
42
+
43
+ end
44
+
@@ -0,0 +1,68 @@
1
+
2
+ module PluginTool
3
+
4
+ class Minimiser
5
+ Context = Java::OrgMozillaJavascript::Context
6
+
7
+ def initialize
8
+ # Load UglifyJS into a JavaScript interpreter
9
+ raise "Another JS Context is active" unless nil == Context.getCurrentContext()
10
+ @cx = Context.enter();
11
+ @cx.setLanguageVersion(Context::VERSION_1_7)
12
+ @javascript_scope = @cx.initStandardObjects()
13
+ ['js_min.js','uglifyjs/parse-js.js','uglifyjs/process.js','uglifyjs/squeeze-more.js'].each do |filename|
14
+ js = File.open("#{File.dirname(__FILE__)}/#{filename}") { |f| f.read }
15
+ @cx.evaluateString(@javascript_scope, js, "<#{filename}>", 1, nil);
16
+ end
17
+ @js_min = @javascript_scope.get("js_min", @javascript_scope);
18
+ end
19
+
20
+ def process(data, filename)
21
+ if filename =~ /\.js\z/
22
+ # JavaScript - use UglifyJS loaded into the JavaScript interpreter
23
+ @js_min.call(@cx, @javascript_scope, @javascript_scope, [data])
24
+
25
+ elsif filename =~ /\.html\z/
26
+ # Simple processing of HTML
27
+ # Remove HTML comments
28
+ html = data.gsub(/\<\!\-\-.+?\-\-\>/m,'')
29
+ # Remove indents
30
+ html.gsub!(/^\s+/,'')
31
+ # Remove any unnecessary line breaks (fairly conservative)
32
+ html.gsub!(/\>[\r\n]+\</m,'><')
33
+ html.gsub!(/([\>}])[\r\n]+([\<{])/m,'\1\2')
34
+ html
35
+
36
+ elsif filename =~ /\.css\z/
37
+ # Simple processing of CSS
38
+ css = data.gsub(/(^|\s)\/\*.+?\*\/($|\s)/m,'') # remove C style comments
39
+ out = []
40
+ css.split(/[\r\n]+/).each do |line|
41
+ line.chomp!; line.gsub!(/^\s+/,''); line.gsub!(/\s+$/,'')
42
+ line.gsub!(/\s+/,' ') # contract spaces
43
+ line.gsub!(/\s*:\s*/,':') # remove unnecessary spaces
44
+ if line =~ /\S/
45
+ out << line
46
+ end
47
+ end
48
+ css = out.join("\n")
49
+ # Remove unnecessary line endings
50
+ css.gsub!(/[\r\n]*(\{[^\}]+\})/m) do |m|
51
+ $1.gsub(/[\r\n]/m,'')
52
+ end
53
+ css
54
+
55
+ else
56
+ # No processing
57
+ data
58
+ end
59
+ end
60
+
61
+ def finish
62
+ Context.exit()
63
+ end
64
+
65
+ end
66
+
67
+ end
68
+
@@ -0,0 +1,8 @@
1
+
2
+ module PluginTool
3
+
4
+ def self.beep
5
+ $stdout.write("\07")
6
+ end
7
+
8
+ end
@@ -0,0 +1,88 @@
1
+
2
+ module PluginTool
3
+
4
+ def self.make_new_plugin(plugin_name)
5
+ unless plugin_name =~ /\A[a-z0-9_]+\z/ && plugin_name.length > 8
6
+ end_on_error "Bad plugin name - must use a-z0-9_ only, and more than 8 characters."
7
+ end
8
+ if File.exist?(plugin_name)
9
+ end_on_error "File or directory #{plugin_name} already exists"
10
+ end
11
+ FileUtils.mkdir(plugin_name)
12
+ ['js', 'static', 'template', 'test'].each do |dir|
13
+ FileUtils.mkdir("#{plugin_name}/#{dir}")
14
+ end
15
+ random = java.security.SecureRandom.new()
16
+ rbytes = Java::byte[20].new
17
+ random.nextBytes(rbytes)
18
+ install_secret = String.from_java_bytes(rbytes).unpack('H*').join
19
+ plugin_url_fragment = plugin_name.gsub('_','-')
20
+ File.open("#{plugin_name}/plugin.json",'w') do |file|
21
+ file.write(<<__E)
22
+ {
23
+ "pluginName": "#{plugin_name}",
24
+ "pluginAuthor": "TODO Your Company",
25
+ "pluginVersion": 1,
26
+ "displayName": "#{plugin_name.split('_').map {|e| e.capitalize} .join(' ')}",
27
+ "displayDescription": "TODO Longer description of plugin",
28
+ "installSecret": "#{install_secret}",
29
+ "apiVersion": 4,
30
+ "load": [
31
+ "js/#{plugin_name}.js"
32
+ ],
33
+ "respond": ["/do/#{plugin_url_fragment}"]
34
+ }
35
+ __E
36
+ end
37
+ File.open("#{plugin_name}/js/#{plugin_name}.js",'w') do |file|
38
+ file.write(<<__E)
39
+
40
+ P.respond("GET", "/do/#{plugin_url_fragment}/example", [
41
+ ], function(E) {
42
+ E.render({
43
+ pageTitle: "Example page"
44
+ });
45
+ });
46
+
47
+ __E
48
+ end
49
+ File.open("#{plugin_name}/template/example.html",'w') do |file|
50
+ file.write(<<__E)
51
+ <p>This is an example template.</p>
52
+ __E
53
+ end
54
+ File.open("#{plugin_name}/test/#{plugin_name}_test1.js",'w') do |file|
55
+ file.write(<<__E)
56
+
57
+ t.test(function() {
58
+
59
+ // For documentation, see
60
+ // http://docs.haplo.org/dev/plugin/tests
61
+
62
+ t.assert(true);
63
+
64
+ });
65
+
66
+ __E
67
+ end
68
+ File.open("#{plugin_name}/requirements.schema",'w') do |file|
69
+ file.write("\n\n\n")
70
+ end
71
+ puts <<__E
72
+
73
+ Plugin #{plugin_name} has been created. Run
74
+
75
+ haplo-plugin -p #{plugin_name}
76
+
77
+ to upload it to the server, then visit
78
+
79
+ https://<HOSTNAME>/do/#{plugin_url_fragment}/example
80
+
81
+ to see a sample page.
82
+
83
+ See http://docs.haplo.org/dev/plugin for more information.
84
+
85
+ __E
86
+ end
87
+
88
+ end
@@ -0,0 +1,89 @@
1
+
2
+ module PluginTool
3
+
4
+ @@notification_options = nil
5
+ @@notification_have_suppressed_first_system_audit_entry = false
6
+ @@notification_announce_reconnect = false
7
+
8
+ def self.start_notifications(options)
9
+ @@notification_options = options
10
+ @@notification_queue_name = nil
11
+ Thread.new do
12
+ while true
13
+ begin
14
+ self.do_notifications
15
+ rescue => e
16
+ puts "NOTICE: Lost notification connection, will attempt to reconnect soon."
17
+ @@notification_announce_reconnect = true
18
+ sleep(5) # throttle errors
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ def self.do_notifications
25
+ http = make_http_connection
26
+ puts "NOTICE: Notification connection established." if @@notification_announce_reconnect
27
+ while true
28
+ sleep(0.25) # small throttle of requests
29
+ path = '/api/development-plugin-loader/get-notifications'
30
+ path << "?queue=#{@@notification_queue_name}" if @@notification_queue_name
31
+ request = Net::HTTP::Get.new(path)
32
+ setup_request(request)
33
+ # Server uses long-polling, and will respond after a long timeout, or when a notification
34
+ # has been queued for sending to this process.
35
+ response = http.request(request)
36
+ if response.kind_of?(Net::HTTPOK)
37
+ @@notification_queue_name = response['X-Queue-Name']
38
+ begin
39
+ decode_and_handle_notifications response.body
40
+ rescue
41
+ puts "NOTICE: Error handling notification from server."
42
+ end
43
+ else
44
+ raise "Bad response"
45
+ end
46
+ end
47
+ end
48
+
49
+ def self.decode_and_handle_notifications(encoded)
50
+ size = encoded.length
51
+ pos = 0
52
+ while pos < (size - 12)
53
+ type = encoded[pos, 4]
54
+ data_size = encoded[pos + 4, 8].to_i(16)
55
+ data = encoded[pos + 12, data_size]
56
+ pos += 12 + data_size
57
+ handle_notification(type, data)
58
+ end
59
+ end
60
+
61
+ def self.handle_notification(type, data)
62
+ case type
63
+ when 'log '
64
+ # Output from console.log()
65
+ puts "LOG:#{data}"
66
+ when 'audt'
67
+ decoded = JSON.parse(data)
68
+ kind = decoded.find { |name,value| name == 'auditEntryType' }.last
69
+ if kind =~ /\A[A-Z\-]+\z/
70
+ # System audit entry - suppressed by default
71
+ unless @@notification_options.show_system_audit
72
+ unless @@notification_have_suppressed_first_system_audit_entry
73
+ @@notification_have_suppressed_first_system_audit_entry = true
74
+ puts "NOTICE: System audit trail entries are not being shown. Run with --show-system-audit to display."
75
+ end
76
+ return
77
+ end
78
+ end
79
+ puts "AUDIT -------------------------------------------"
80
+ decoded.each do |key, value|
81
+ puts sprintf("%22s: %s", key, value.to_s)
82
+ end
83
+ else
84
+ puts "WARNING: Unknown notification received from server. Upgrade the plugin tool using 'jgem update haplo'."
85
+ sleep(5) # throttle problematic responses
86
+ end
87
+ end
88
+
89
+ end
@@ -0,0 +1,62 @@
1
+
2
+ module PluginTool
3
+
4
+ PACKING_ACCEPTABLE_FILENAME = /\A(js|static|template|test|data)\/([a-z0-9_-]+\/)*[a-z0-9_-]+\.[a-z0-9]+\z/
5
+ PACKING_ACCEPTABLE_EXCEPTIONS = ['plugin.json', 'requirements.schema', 'global.js', 'certificates-temp-http-api.pem']
6
+ PACKING_EXCLUDE = ['developer.json']
7
+
8
+ def self.pack_plugin(plugin_name, output_directory)
9
+ # Get filenames and sort
10
+ files = Dir.glob("#{plugin_name}/**/*").map do |filename|
11
+ if File.file? filename
12
+ filename[plugin_name.length+1, filename.length]
13
+ else
14
+ nil
15
+ end
16
+ end .select { |f| !PACKING_EXCLUDE.include?(f) }.compact.sort
17
+ # Check each filename is acceptable
18
+ files.each do |filename|
19
+ unless filename =~ PACKING_ACCEPTABLE_FILENAME || PACKING_ACCEPTABLE_EXCEPTIONS.include?(filename)
20
+ puts "File '#{filename}' has an unacceptable filename"
21
+ exit 1
22
+ end
23
+ end
24
+ # Clean output directory
25
+ output_plugin_dir = "#{output_directory}/#{plugin_name}"
26
+ puts "Output directory: #{output_plugin_dir}"
27
+ if File.exist? output_plugin_dir
28
+ puts "Removing old output directory #{output_plugin_dir}"
29
+ FileUtils.rm_r(output_plugin_dir)
30
+ end
31
+ # Make file structure
32
+ FileUtils.mkdir(output_plugin_dir)
33
+ # Process each file, building a manifest
34
+ puts "Processing files:"
35
+ manifest = ''
36
+ minimiser = PluginTool::Minimiser.new
37
+ files.each do |filename|
38
+ puts " #{filename}"
39
+ data = File.open("#{plugin_name}/#{filename}") { |f| f.read }
40
+ # Minimise file?
41
+ unless filename =~ /\Ajs\//
42
+ data = minimiser.process(data, filename)
43
+ end
44
+ hash = Digest::SHA256.hexdigest(data)
45
+ # Make sure output directory exists, write file
46
+ output_pathname = "#{output_plugin_dir}/#{filename}"
47
+ output_directory = File.dirname(output_pathname)
48
+ FileUtils.mkdir_p(output_directory) unless File.directory?(output_directory)
49
+ File.open(output_pathname, "w") { |f| f.write data }
50
+ # Filename entry in Manifest
51
+ manifest << "F #{hash} #{filename}\n"
52
+ end
53
+ minimiser.finish
54
+ # Write manifest and version
55
+ File.open("#{output_plugin_dir}/manifest", "w") { |f| f.write manifest }
56
+ version = Digest::SHA256.hexdigest(manifest)
57
+ File.open("#{output_plugin_dir}/version", "w") { |f| f.write "#{version}\n" }
58
+ # All done
59
+ puts "Version: #{version}\nPlugin packed."
60
+ end
61
+
62
+ end
@@ -0,0 +1,227 @@
1
+
2
+ module PluginTool
3
+
4
+ class Plugin
5
+ DEFAULT_PLUGIN_LOAD_PRIORITY = 9999999
6
+
7
+ def initialize(name, options)
8
+ @name = name
9
+ @options = options
10
+ end
11
+ attr_accessor :name
12
+ attr_accessor :plugin_dir
13
+ attr_accessor :loaded_plugin_id
14
+
15
+ # ---------------------------------------------------------------------------------------------------------
16
+
17
+ @@pending_apply = []
18
+
19
+ # ---------------------------------------------------------------------------------------------------------
20
+
21
+ def start
22
+ # Check to see if the plugin is valid
23
+ unless File.file?("#{@name}/plugin.json")
24
+ end_on_error "Plugin #{@name} does not exist (no plugin.json file)"
25
+ end
26
+ # Setup for using the plugin
27
+ @plugin_dir = @name
28
+ end_on_error "logic error" unless @plugin_dir =~ /\A[a-zA-Z0-9_-]+\z/
29
+ @loaded_plugin_id = nil
30
+ end
31
+
32
+ def plugin_load_priority
33
+ pj = File.open("#{@plugin_dir}/plugin.json") { |f| JSON.parse(f.read) }
34
+ pj['loadPriority'] || DEFAULT_PLUGIN_LOAD_PRIORITY
35
+ end
36
+
37
+ def print_banner
38
+ puts "Plugin: #{@plugin_dir}"
39
+ end
40
+
41
+ def setup_for_server
42
+ # Make the first empty manifest (may be replaced from server)
43
+ @current_manifest = {}
44
+
45
+ # If minimisation is active, clear the current manifest so all files are uploaded again.
46
+ if @options.minimiser != nil
47
+ @current_manifest = {}
48
+ end
49
+
50
+ # See if the plugin has already been registered with the server
51
+ s_found_info = PluginTool.post_with_json_response("/api/development-plugin-loader/find-registration", {:name => @name})
52
+ if s_found_info["found"]
53
+ # Store info returned by the server
54
+ @loaded_plugin_id = s_found_info["plugin_id"]
55
+ @current_manifest = s_found_info["manifest"]
56
+ end
57
+
58
+ # If there isn't an existing plugin registered, create a new one
59
+ if @loaded_plugin_id == nil
60
+ s_create_info = PluginTool.post_with_json_response("/api/development-plugin-loader/create")
61
+ end_on_error "Couldn't communicate successfully with server." if s_create_info["protocol_error"]
62
+ end_on_error "Failed to create plugin on server" unless s_create_info["plugin_id"] != nil
63
+ @loaded_plugin_id = s_create_info["plugin_id"]
64
+ end
65
+ end
66
+
67
+ # ---------------------------------------------------------------------------------------------------------
68
+
69
+ def exclude_files_from_syntax_check
70
+ @exclude_files_from_syntax_check ||= begin
71
+ # developer.json file might contain some files which should not be syntax checked
72
+ exclude_files_from_check = []
73
+ developer_json_pathname = "#{@plugin_dir}/developer.json"
74
+ if File.exist? developer_json_pathname
75
+ developer_json = JSON.parse(File.read(developer_json_pathname))
76
+ if developer_json['excludeFromSyntaxCheck'].kind_of?(Array)
77
+ exclude_files_from_check = developer_json['excludeFromSyntaxCheck']
78
+ end
79
+ end
80
+ exclude_files_from_check
81
+ end
82
+ end
83
+
84
+ # ---------------------------------------------------------------------------------------------------------
85
+
86
+ def command(cmd)
87
+ case cmd
88
+ when 'license-key'
89
+ application_id = @options.args.first
90
+ if application_id == nil || application_id !~ /\A\d+\z/
91
+ end_on_error "Numeric application ID must be specified"
92
+ end
93
+ generate_license_key(application_id)
94
+
95
+ when 'pack'
96
+ PluginTool.pack_plugin(@name, @options.output)
97
+
98
+ when 'reset-db'
99
+ puts "Resetting database on server for #{@name}..."
100
+ reset_result = PluginTool.post_with_json_response("/api/development-plugin-loader/resetdb/#{@loaded_plugin_id}")
101
+ end_on_error "Couldn't remove old database tables" unless reset_result["result"] == 'success'
102
+ apply_result = PluginTool.post_with_json_response("/api/development-plugin-loader/apply", :plugins => @loaded_plugin_id)
103
+ end_on_error "Couldn't apply changes" unless apply_result["result"] == 'success'
104
+ puts "Done."
105
+
106
+ when 'uninstall'
107
+ puts "Uninstalling plugin #{@name} from server..."
108
+ reset_result = PluginTool.post_with_json_response("/api/development-plugin-loader/uninstall/#{@loaded_plugin_id}")
109
+ end_on_error "Couldn't uninstall plugin" unless reset_result["result"] == 'success'
110
+ puts "Done."
111
+
112
+ when 'test'
113
+ puts "Running tests..."
114
+ params = {}
115
+ params["test"] = @options.args.first unless @options.args.empty?
116
+ test_result = PluginTool.post_with_json_response("/api/development-plugin-loader/run-tests/#{@loaded_plugin_id}", params)
117
+ end_on_error "Couldn't run tests" unless test_result["result"] == 'success'
118
+ puts
119
+ puts test_result["output"] || ''
120
+ puts test_result["summary"] || "(unknown results)"
121
+
122
+ when 'develop'
123
+ # do nothing here
124
+
125
+ else
126
+ end_on_error "Unknown command '#{cmd}'"
127
+
128
+ end
129
+ end
130
+
131
+ # ---------------------------------------------------------------------------------------------------------
132
+
133
+ def develop_setup
134
+ end
135
+
136
+ def develop_scan_and_upload(first_run)
137
+ should_apply = first_run
138
+ next_manifest = PluginTool.generate_manifest(@plugin_dir)
139
+ if !(next_manifest.has_key?("plugin.json"))
140
+ # If the plugin.json file is deleted, just uninstall the plugin from the server
141
+ command('uninstall')
142
+ @is_uninstalled = true
143
+ return
144
+ elsif @is_uninstalled
145
+ should_apply = true
146
+ end
147
+ changes = PluginTool.determine_manifest_changes(@current_manifest, next_manifest)
148
+ upload_failed = false
149
+ changes.each do |filename, action|
150
+ filename =~ /\A(.*?\/)?([^\/]+)\z/
151
+ params = {:filename => $2}
152
+ params[:directory] = $1.gsub(/\/\z/,'') if $1
153
+ if action == :delete
154
+ puts " #{@name}: Deleting #{filename}"
155
+ PluginTool.post_with_json_response("/api/development-plugin-loader/delete-file/#{@loaded_plugin_id}", params)
156
+ else
157
+ puts " #{@name}: Uploading #{filename}"
158
+ data = File.open("#{@plugin_dir}/#{filename}") { |f| f.read }
159
+ hash = action
160
+ # Minimise file before uploading?
161
+ if @options.minimiser != nil && filename =~ /\A(static|template)\//
162
+ size_before = data.length
163
+ data = @options.minimiser.process(data, filename)
164
+ size_after = data.length
165
+ hash = Digest::SHA256.hexdigest(data)
166
+ puts " minimisation: #{size_before} -> #{size_after} (#{(size_after * 100) / size_before}%)"
167
+ end
168
+ r = PluginTool.post_with_json_response("/api/development-plugin-loader/put-file/#{@loaded_plugin_id}", params, {:file => [filename, data]})
169
+ if r["result"] == 'success'
170
+ # If the file was uploaded successfully, but the hash didn't match, abort now
171
+ end_on_error "#{@name}: Disagreed with server about uploaded file hash: local=#{hash}, remote=#{r["hash"]}" unless hash == r["hash"]
172
+ else
173
+ # Otherwise mark as a failed upload to stop an apply operation which will fail
174
+ upload_failed = true
175
+ end
176
+ PluginTool.syntax_check(self, filename) if filename =~ /\.(js|hsvt)\z/i
177
+ end
178
+ end
179
+ if upload_failed
180
+ puts "\n#{@name}: Not applying changes due to failure\n\n"
181
+ else
182
+ if !(changes.empty?) || should_apply
183
+ @@pending_apply.push(self) unless @@pending_apply.include?(self)
184
+ end
185
+ end
186
+ @current_manifest = next_manifest
187
+ end
188
+
189
+ # ---------------------------------------------------------------------------------------------------------
190
+
191
+ def self.do_apply
192
+ return if @@pending_apply.empty?
193
+ puts "Applying changes on server: #{@@pending_apply.map { |p| p.name } .join(', ')}"
194
+ r = PluginTool.post_with_json_response("/api/development-plugin-loader/apply", {
195
+ :plugins => @@pending_apply.map { |p| p.loaded_plugin_id }.join(' ')
196
+ })
197
+ if r["result"] == 'success'
198
+ @@pending_apply = []
199
+ else
200
+ puts "\n\nDidn't apply changes on server\n\n"
201
+ PluginTool.beep
202
+ end
203
+ end
204
+
205
+ # ---------------------------------------------------------------------------------------------------------
206
+
207
+ def generate_license_key(application_id)
208
+ info = File.open("#{@plugin_dir}/plugin.json") { |f| JSON.parse(f.read) }
209
+ if info["installSecret"] == nil
210
+ end_on_error "#{@name}: No installSecret specified in plugin.json"
211
+ end
212
+ license_key = HMAC::SHA1.sign(info["installSecret"], "application:#{application_id}")
213
+ puts <<__E
214
+
215
+ Plugin: #{@name}
216
+ Application: #{application_id}
217
+ License key: #{license_key}
218
+ __E
219
+ end
220
+
221
+ def end_on_error(err)
222
+ puts err
223
+ exit 1
224
+ end
225
+ end
226
+
227
+ end