kstrano 1.0.12 → 1.1.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
data/bin/kumafy CHANGED
@@ -9,86 +9,108 @@ require 'uri'
9
9
 
10
10
  HighLine.track_eof = false
11
11
 
12
- DEPLOY_GIST = "https://raw.github.com/Kunstmaan/kStrano/master/config/deploy.rb"
13
- PRODUCTION_GIST = "https://raw.github.com/Kunstmaan/kStrano/master/config/production.rb"
14
- STAGING_GIST = "https://raw.github.com/Kunstmaan/kStrano/master/config/staging.rb"
15
-
16
- BUILD_GIST = "https://raw.github.com/Kunstmaan/kStrano/master/config/build.xml"
17
- PHPCS_GIST = "https://raw.github.com/Kunstmaan/kStrano/master/config/phpcs.xml"
18
- PHPMD_GIST = "https://raw.github.com/Kunstmaan/kStrano/master/config/phpmd.xml"
19
- PHPDOX_GIST = "https://raw.github.com/Kunstmaan/kStrano/master/config/phpdox.xml"
20
- PHPUNIT_GIST = "https://raw.github.com/Kunstmaan/kStrano/master/config/phpunit.xml.dist"
21
- APPTEST_GIST = "https://raw.github.com/Kunstmaan/kStrano/master/config/app_test.php"
22
- BEHAT_GIST = "https://raw.github.com/Kunstmaan/kStrano/master/config/behat.yml-dist"
23
-
24
- def update_capfile(base, context, force)
12
+ BASE_PATH = File.realpath(File.dirname(Gem.find_files('kstrano.rb').last.to_s) + '/../') # the directory of this GEM
13
+ RESOURCES_BASE_PATH = "#{BASE_PATH}/resources" # the directory of the resources folder
14
+ RECIPES = {
15
+ "symfony2" => {
16
+ "config_path" => "app/config"
17
+ },
18
+ "play" => {
19
+ "config_path" => "conf"
20
+ },
21
+ "drupal" => {
22
+ "config_path" => "app/config"
23
+ },
24
+ "magento" => {
25
+ "config_path" => "app/config"
26
+ }
27
+ } # all the supported recipes
28
+
29
+ def update_capfile(ui, base, context, force)
25
30
  file = File.join(base, "Capfile")
26
31
 
27
- if !File.exists?("Capfile")
28
- abort "Make sure the project has been capified or capifonied."
29
- else
30
- includestr = "load Gem.find_files('kstrano.rb').last.to_s"
31
- fcontent = ""
32
+ if File.exists?(file)
33
+ warn "[skip] #{file} already exists"
34
+
35
+ content = ""
32
36
  File.open(file, "r") do |f|
33
37
  f.each do |line|
34
- fcontent << line
35
- if line.include? includestr
36
- abort "This project is already kumafied!" unless force
37
- return
38
- end
38
+ content << line
39
39
  end
40
40
  end
41
41
 
42
- File.open(file, "w") do |f|
43
- fcontent.each_line do |line|
44
- if line.include? "load 'app/config/deploy'"
45
- f.print includestr + "\r\n"
46
- end
47
- f.print line
48
- end
42
+ context["recipe"] = content.match("kstrano_(#{RECIPES.keys.join('|')})")[1]
43
+ else
44
+ context["recipe"] ||= ui.ask("What type of application is it? (#{RECIPES.keys.join(', ')})").downcase
45
+
46
+ if !RECIPES.include? context['recipe']
47
+ abort "No such recipe (#{context['recipe']}"
49
48
  end
49
+
50
+ recipe = context["recipe"]
51
+ content = unindent(<<-FILE)
52
+ load 'deploy' if respond_to?(:namespace) # cap2 differentiator
53
+
54
+ require 'kstrano_#{recipe}'
55
+ load '#{RECIPES[recipe]['config_path']}/deploy'
56
+ FILE
57
+
58
+ File.open(file, "w") { |f| f.write(content)}
50
59
  end
51
60
  end
52
61
 
53
62
  def update_deploy_config(ui, base, context, force)
54
-
55
- deploy = File.join(base, "app", "config", "deploy.rb")
63
+
64
+ deploy = File.join(base, RECIPES[context['recipe']]['config_path'], "deploy.rb")
56
65
  write_deploy = true
57
-
66
+
58
67
  if write_deploy
59
- dirname = File.basename(Dir.getwd)
60
- deploy_gist = get_plain_secure(DEPLOY_GIST).body
68
+ deploy_config_file = read_resource_file("deploy.rb")
61
69
  context["app_name"] ||= ui.ask("What's the name of the application?")
62
70
  app_name = context["app_name"]
63
- deploy_gist.gsub!(/(:application,\s)(\"\")/, '\1' + "\"#{app_name}\"")
64
- deploy_gist.gsub!(/(:admin_runner,\s)(\"\")/, '\1' + "\"#{app_name}\"")
71
+ deploy_config_file.gsub!(/(:application,\s)(\"\")/, '\1' + "\"#{app_name}\"")
72
+ deploy_config_file.gsub!(/(:admin_runner,\s)(\"\")/, '\1' + "\"#{app_name}\"")
73
+ deploy_config_file.gsub!(/(:stage_dir,\s)(\"\")/, '\1' + "\"#{RECIPES[context['recipe']]['config_path']}/deploy\"")
65
74
 
66
75
  newrelic_appname = ui.ask("What's the name of the application in new relic?")
67
76
  if !newrelic_appname.nil? && !newrelic_appname.empty?
68
- deploy_gist.gsub!(/(:newrelic_appname,\s)(\"\")/, '\1' + "\"#{newrelic_appname}\"")
77
+ deploy_config_file.gsub!(/(:newrelic_appname,\s)(\"\")/, '\1' + "\"#{newrelic_appname}\"")
69
78
  context["newrelic_appname"] = newrelic_appname
70
79
  end
71
80
 
72
81
  newrelic_license_key = ui.ask("What's the license key of your newrelic account (can be found under 'Account settings')?")
73
82
  if !newrelic_license_key.nil? && !newrelic_license_key.empty?
74
- deploy_gist.gsub!(/(:newrelic_license_key,\s)(\"\")/, '\1' + "\"#{newrelic_license_key}\"")
83
+ deploy_config_file.gsub!(/(:newrelic_license_key,\s)(\"\")/, '\1' + "\"#{newrelic_license_key}\"")
75
84
  context["newrelic_license_key"] = newrelic_license_key
76
85
  end
77
-
86
+
87
+ extra_config_file = "#{RESOURCES_BASE_PATH}/#{context["recipe"]}/deploy.rb"
88
+ if File.exists? extra_config_file
89
+ deploy_config_file << "\n\n"
90
+ deploy_config_file << "# #{context['recipe'].capitalize} config"
91
+ deploy_config_file << "\n"
92
+
93
+ File.open(extra_config_file, "r") do |f|
94
+ f.each do |line|
95
+ deploy_config_file << line
96
+ end
97
+ end
98
+ end
99
+
78
100
  File.open(deploy, "w") do |f|
79
- deploy_gist.each_line do |line|
101
+ deploy_config_file.each_line do |line|
80
102
  f.print line
81
103
  end
82
104
  end
83
105
  end
84
-
85
- deploy_dir = File.join(base, "app", "config", "deploy")
106
+
107
+ deploy_dir = File.join(base, RECIPES[context['recipe']]['config_path'], "deploy")
86
108
  Dir.mkdir(deploy_dir) unless File.directory?(deploy_dir)
87
-
109
+
88
110
  {
89
- "production" => PRODUCTION_GIST,
90
- "staging" => STAGING_GIST
91
- }.each do |env, gist|
111
+ "production" => "deploy_production.rb",
112
+ "staging" => "deploy_staging.rb"
113
+ }.each do |env, source_file|
92
114
  file = File.join(deploy_dir, "#{env}.rb")
93
115
  write = true
94
116
 
@@ -100,25 +122,25 @@ def update_deploy_config(ui, base, context, force)
100
122
  end
101
123
 
102
124
  if write
103
- gist_body = get_plain_secure(gist).body
125
+ content = read_resource_file(source_file)
104
126
  server = ui.ask("On which server is the #{env} environment deployed?")
105
- if !server.match(/^.*\.kunstmaan\.be$/) && !server.match(/^.*\.kunstmaan\.com$/)
127
+ if !server.match(/^.*\.kunstmaan\.be$/) && !server.match(/^.*\.kunstmaan\.com$/)
106
128
  server = "#{server}.cloud.kunstmaan.com"
107
129
  end
108
130
 
109
- gist_body.gsub!(/(:domain,\s)(\"\")/, '\1' + "\"#{server}\"")
131
+ content.gsub!(/(:domain,\s)(\"\")/, '\1' + "\"#{server}\"")
110
132
 
111
133
  File.open(file, "w") do |f|
112
- gist_body.each_line do |line|
134
+ content.each_line do |line|
113
135
  f.print line
114
136
  end
115
137
  end
116
138
  end
117
139
  end
118
-
140
+
119
141
  end
120
142
 
121
- def update_jenkins_config(ui, base, context, force)
143
+ def update_symfony2(ui, base, context, force)
122
144
  file = File.join(base, "build.xml")
123
145
  write = true
124
146
 
@@ -130,52 +152,82 @@ def update_jenkins_config(ui, base, context, force)
130
152
  end
131
153
 
132
154
  if write
133
- gist_body = get_plain_secure(BUILD_GIST).body
134
-
155
+ build_file = read_resource_file("#{context['recipe']}/build.xml")
156
+
135
157
  context["app_name"] ||= ui.ask("What's the name of the application?")
136
158
  app_name = context["app_name"]
137
- gist_body.gsub!(/(\<project\sname=)(\"\")/, '\1' + "\"#{app_name}\"")
138
-
159
+ build_file.gsub!(/(\<project\sname=)(\"\")/, '\1' + "\"#{app_name}\"")
160
+
139
161
  File.open(file, "w") do |f|
140
- gist_body.each_line do |line|
162
+ build_file.each_line do |line|
141
163
  f.print line
142
164
  end
143
165
  end
144
166
  end
145
-
167
+
146
168
  build_dir = File.join(base, "build")
147
169
  web_dir = File.join(base, "web")
148
170
  Dir.mkdir(build_dir) unless File.directory?(build_dir)
149
171
  Dir.mkdir(web_dir) unless File.directory?(web_dir)
150
-
151
- {
152
- File.join(build_dir, "phpcs.xml") => PHPCS_GIST,
153
- File.join(build_dir, "phpmd.xml") => PHPMD_GIST,
154
- File.join(build_dir, "phpdox.xml") => PHPDOX_GIST,
155
- File.join(base, "app", "phpunit.xml.dist") => PHPUNIT_GIST,
156
- File.join(web_dir, "app_test.php") => APPTEST_GIST,
157
- File.join(base, "behat.yml-dist") => BEHAT_GIST
158
- }.each do |file, gist|
172
+
173
+ copy_resources({
174
+ File.join(build_dir, "phpcs.xml") => "#{context['recipe']}/phpcs.xml",
175
+ File.join(build_dir, "phpmd.xml") => "#{context['recipe']}/phpmd.xml",
176
+ File.join(build_dir, "phpdox.xml") => "#{context['recipe']}/phpdox.xml",
177
+ File.join(base, "app", "phpunit.xml.dist") => "#{context['recipe']}/phpunit.xml.dist",
178
+ File.join(web_dir, "app_test.php") => "#{context['recipe']}/app_test.php",
179
+ File.join(base, "behat.yml-dist") => "#{context['recipe']}/behat.yml-dist"
180
+ }, ui, context)
181
+ end
182
+
183
+ def update_play(ui, base, context, force)
184
+ copy_resources({
185
+ File.join(base, "start.sh") => "#{context['recipe']}/start.sh",
186
+ File.join(base, "stop.sh") => "#{context['recipe']}/stop.sh"
187
+ }, ui, context)
188
+ end
189
+
190
+ def copy_resources(resource_map, ui, context)
191
+ resource_map.each do |destination_file, source_file|
159
192
  write = true
160
-
161
- if File.exists?(file)
162
- overwrite = ui.ask("The file #{file} already exists, do you want to override it? ") { |q| q.default = 'n' }
193
+
194
+ if File.exists?(destination_file)
195
+ overwrite = ui.ask("The file #{destination_file} already exists, do you want to override it? ") { |q| q.default = 'n' }
163
196
  if !overwrite.match(/^y/)
164
197
  write = false
165
198
  end
166
199
  end
167
-
200
+
168
201
  if write
169
- gist_body = get_plain_secure(gist).body
170
-
171
- File.open(file, "w") do |f|
172
- gist_body.each_line do |line|
173
- f.print line
202
+ content = read_resource_file("#{source_file}")
203
+
204
+ File.open(destination_file, "w") do |f|
205
+ content.each_line do |line|
206
+ f.print line.sub("{{application_name}}", context['app_name'])
174
207
  end
175
208
  end
176
209
  end
177
210
  end
178
-
211
+ end
212
+
213
+ def read_resource_file(filename)
214
+ read_file("#{RESOURCES_BASE_PATH}/#{filename}")
215
+ end
216
+
217
+ def read_file(file)
218
+ content = ""
219
+ File.open(file, "r") do |f|
220
+ f.each do |line|
221
+ content << line
222
+ end
223
+ end
224
+
225
+ content
226
+ end
227
+
228
+ def unindent(string)
229
+ indentation = string[/\A\s*/]
230
+ string.strip.gsub(/^#{indentation}/, "")
179
231
  end
180
232
 
181
233
  def get_plain_secure(url)
@@ -198,27 +250,6 @@ def validate_path
198
250
  end
199
251
  end
200
252
 
201
- def update_vendors(base)
202
- vendors = File.join(base, "bin", "vendors")
203
- if File.directory?(vendors)
204
- fcontent = ""
205
- File.open(vendors, "r") do |f|
206
- f.each do |line|
207
- fcontent << line
208
- end
209
- end
210
-
211
- File.open(vendors, "w") do |f|
212
- fcontent.each_line do |line|
213
- f.print line
214
- if line.include? "git clone %s %s"
215
- f.print " system(sprintf('cd %s && git config core.filemode false', escapeshellarg($installDir)));" + "\r\n"
216
- end
217
- end
218
- end
219
- end
220
- end
221
-
222
253
  force = false
223
254
 
224
255
  OptionParser.new do |opts|
@@ -228,7 +259,7 @@ OptionParser.new do |opts|
228
259
  puts opts
229
260
  exit 0
230
261
  end
231
-
262
+
232
263
  opts.on("-c", "--config", "Creates the configuration files needed for deployment") do
233
264
  validate_path
234
265
  ui = HighLine.new
@@ -237,16 +268,7 @@ OptionParser.new do |opts|
237
268
  update_deploy_config ui, base, context, force
238
269
  exit 0
239
270
  end
240
-
241
- opts.on("-j", "--jenkins", "Creates the jenkins configuration files for this project") do
242
- validate_path
243
- ui = HighLine.new
244
- base = ARGV.shift
245
- context = Hash.new
246
- update_jenkins_config ui, base, context, force
247
- exit 0
248
- end
249
-
271
+
250
272
  opts.on("-f", "--force", "This will force the kumafying of the project") do
251
273
  force = true
252
274
  end
@@ -270,13 +292,13 @@ if force && File.exists?("Capfile")
270
292
  File.delete("Capfile")
271
293
  end
272
294
 
273
- puts "[start] capifony"
274
- %x(capifony .)
275
- puts "[end] capifony"
276
-
277
- update_capfile base, context, force
295
+ update_capfile ui, base, context, force
278
296
  update_deploy_config ui, base, context, force
279
- update_jenkins_config ui, base, context, force
280
- update_vendors base
297
+
298
+ begin
299
+ send("update_#{context['recipe']}", ui, base, context, force)
300
+ rescue NoMethodError => e
301
+ # Do nothing
302
+ end
281
303
 
282
304
  puts "[done] project kumafied!"
@@ -1,15 +1,15 @@
1
- module Kumastrano
1
+ module KStrano
2
2
  # Using the gem https://github.com/airbrake/airbrake doesn't work because it's made for rails apps, it needs rake etc. + you need to have the i18n gem
3
3
  # This will integrate a very easy command to tell airbrake a deploy has been done
4
4
  class AirbrakeHelper
5
-
5
+
6
6
  require 'net/http'
7
7
  require 'uri'
8
8
  require 'etc'
9
-
9
+
10
10
  def self.notify(api_key, revision, repository, environment = 'production', username = Etc.getlogin.capitalize)
11
11
  uri = URI.parse("http://airbrake.io")
12
-
12
+
13
13
  params = {
14
14
  'api_key' => api_key,
15
15
  'deploy[rails_env]' => environment, # Environment of the deploy (production, staging), this needs to be the current environment
@@ -20,9 +20,9 @@ module Kumastrano
20
20
 
21
21
  post = Net::HTTP::Post.new("/deploys")
22
22
  post.set_form_data(params)
23
-
23
+
24
24
  res = Net::HTTP.start(uri.host, uri.port) {|http| http.request(post)}
25
-
25
+
26
26
  if res.code.to_i == 200
27
27
  return true
28
28
  else
@@ -1,21 +1,21 @@
1
- module Kumastrano
1
+ module KStrano
2
2
  class CampfireHelper
3
-
3
+
4
4
  require 'broach'
5
-
5
+
6
6
  def self.speak(campfire_account, campfire_token, campfire_room, message="")
7
7
  ## extracted this to here, because i don't know how to call capistrano tasks with arguments
8
8
  ## else i just had to make one capistrano task which i could call
9
9
  if !campfire_account.nil? && !campfire_token.nil? && !campfire_room.nil?
10
-
10
+
11
11
  Broach.settings = {
12
12
  'account' => campfire_account,
13
13
  'token' => campfire_token,
14
14
  'use_ssl' => true
15
15
  }
16
-
16
+
17
17
  room = Broach::Room.find_by_name campfire_room
18
-
18
+
19
19
  if !room.nil?
20
20
  room.speak message
21
21
  end
@@ -1,36 +1,36 @@
1
- module Kumastrano
1
+ module KStrano
2
2
  class GitHelper
3
-
3
+
4
4
  require 'cgi'
5
-
5
+
6
6
  def self.fetch
7
7
  %x(git fetch)
8
8
  end
9
-
9
+
10
10
  def self.merge_base(commit1, commit2 = "HEAD")
11
11
  base = %x(git merge-base #{commit1} #{commit2})
12
12
  base.strip
13
13
  end
14
-
14
+
15
15
  def self.commit_hash(commit = "HEAD")
16
16
  hash = %x(git rev-parse #{commit})
17
17
  hash.strip
18
18
  end
19
-
19
+
20
20
  def self.branch_name(commit = "HEAD")
21
21
  name = %x(git name-rev --name-only #{commit})
22
22
  name.strip
23
23
  end
24
-
24
+
25
25
  def self.origin_refspec
26
26
  refspec = %x(git config remote.origin.fetch)
27
27
  refspec.strip
28
28
  end
29
-
29
+
30
30
  def self.origin_url
31
31
  url = %x(git config remote.origin.url)
32
32
  url.strip
33
33
  end
34
-
34
+
35
35
  end
36
36
  end
@@ -1,24 +1,24 @@
1
- module Kumastrano
1
+ module KStrano
2
2
  class JenkinsHelper
3
-
3
+
4
4
  require 'cgi'
5
5
  require 'net/http'
6
6
  require 'uri'
7
7
  require 'json'
8
-
8
+
9
9
  def self.available?(base_uri)
10
10
  res = get_plain("#{base_uri}")
11
11
  res.code.to_i == 200
12
12
  end
13
-
13
+
14
14
  def self.build_and_wait(job_uri, timeout=300, interval=2)
15
15
  success = false
16
16
  last_build_info = nil
17
- prev_build = Kumastrano::JenkinsHelper.last_build_number(job_uri)
18
- Kumastrano::JenkinsHelper.build_job(job_uri)
19
- Kumastrano.poll("A timeout occured", timeout, interval) do
17
+ prev_build = KStrano::JenkinsHelper.last_build_number(job_uri)
18
+ KStrano::JenkinsHelper.build_job(job_uri)
19
+ KStrano.poll("A timeout occured", timeout, interval) do
20
20
  ## wait for the building to be finished
21
- last_build_info = Kumastrano::JenkinsHelper.build_info(job_uri)
21
+ last_build_info = KStrano::JenkinsHelper.build_info(job_uri)
22
22
  result = last_build_info['result'] ## SUCCESS or FAILURE
23
23
  building = last_build_info['building']
24
24
  number = last_build_info['number']
@@ -35,7 +35,7 @@ module Kumastrano
35
35
  end
36
36
  return success, last_build_info
37
37
  end
38
-
38
+
39
39
  def self.fetch_build_hash_from_build_info(build_info, branch_name)
40
40
  actions = build_info['actions']
41
41
 
@@ -47,18 +47,18 @@ module Kumastrano
47
47
  break
48
48
  end
49
49
  end
50
-
50
+
51
51
  build_hash
52
52
  end
53
-
53
+
54
54
  def self.make_safe_job_name(app_name, branch_name)
55
55
  job_name = "#{app_name} (#{branch_name})"
56
56
  job_name.gsub(/[#*\/\\]/, "-") # \/#* is unsafe for jenkins job name, because not uri safe
57
57
  end
58
-
58
+
59
59
  def self.job_url_for_branch(jenkins_base_uri, branch_name)
60
60
  current_job_url = nil
61
- Kumastrano::JenkinsHelper.list_jobs(jenkins_base_uri).each do |job|
61
+ KStrano::JenkinsHelper.list_jobs(jenkins_base_uri).each do |job|
62
62
  name = job["name"]
63
63
  url = job["url"]
64
64
  if /.*\(#{branch_name}\)/.match(name)
@@ -68,10 +68,10 @@ module Kumastrano
68
68
  end
69
69
  current_job_url
70
70
  end
71
-
71
+
72
72
  def self.job_url_for_name(jenkins_base_uri, job_name)
73
73
  current_job_url = nil
74
- Kumastrano::JenkinsHelper.list_jobs(jenkins_base_uri).each do |job|
74
+ KStrano::JenkinsHelper.list_jobs(jenkins_base_uri).each do |job|
75
75
  name = job["name"]
76
76
  url = job["url"]
77
77
  if job_name == name
@@ -81,12 +81,12 @@ module Kumastrano
81
81
  end
82
82
  current_job_url
83
83
  end
84
-
84
+
85
85
  def self.list_jobs(base_uri)
86
86
  res = get_plain("#{base_uri}/api/json?tree=jobs[name,url]")
87
87
  parsed_res = JSON.parse(res.body)["jobs"]
88
88
  end
89
-
89
+
90
90
  def self.create_new_job(base_uri, job_name, config)
91
91
  uri = URI.parse("#{base_uri}/createItem/api/json")
92
92
  request = Net::HTTP::Post.new(uri.path + "?name=#{CGI.escape(job_name)}")
@@ -100,33 +100,33 @@ module Kumastrano
100
100
  puts res.body
101
101
  end
102
102
  end
103
-
103
+
104
104
  def self.retrieve_config_xml(job_uri)
105
105
  res = get_plain("#{job_uri}/config.xml").body
106
106
  end
107
-
107
+
108
108
  def self.build_job(job_uri)
109
109
  res = get_plain("#{job_uri}/build")
110
110
  res.code.to_i == 302
111
111
  end
112
-
112
+
113
113
  def self.last_build_number(job_uri)
114
114
  res = get_plain("#{job_uri}/api/json?tree=lastBuild[number]")
115
115
  parsed_res = JSON.parse(res.body)
116
116
  parsed_res['lastBuild']['number']
117
117
  end
118
-
118
+
119
119
  def self.build_info(job_uri, build="lastBuild")
120
120
  res = get_plain("#{job_uri}/#{build}/api/json")
121
121
  parsed_res = JSON.parse(res.body)
122
122
  end
123
-
123
+
124
124
  private
125
-
125
+
126
126
  def self.get_plain(uri)
127
127
  uri = URI.parse uri
128
128
  res = Net::HTTP.start(uri.host, uri.port) { |http| http.get(uri.path, {}) }
129
129
  end
130
-
131
- end
130
+
131
+ end
132
132
  end
@@ -1,15 +1,15 @@
1
- module Kumastrano
2
-
3
- def poll(msg=nil, seconds=10.0, interval_seconds=1.0)
1
+ module KStrano
2
+
3
+ def poll(msg=nil, seconds=10.0, interval_seconds=1.0)
4
4
  (seconds / interval_seconds).to_i.times do
5
5
  result = yield
6
6
  return if result
7
7
  sleep interval_seconds
8
8
  end
9
- msg ||= "polling failed after #{seconds} seconds"
9
+ msg ||= "polling failed after #{seconds} seconds"
10
10
  raise msg
11
11
  end
12
-
12
+
13
13
  def say(text, prefix='--> ')
14
14
  Capistrano::CLI.ui.say("#{prefix}#{text}")
15
15
  end
@@ -23,5 +23,5 @@ module Kumastrano
23
23
  end
24
24
 
25
25
  module_function :poll, :say, :ask
26
-
26
+
27
27
  end