blufin-lib 1.7.6 → 1.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8895eedbca233a947357cfc7ee7ca2fcea304563
4
- data.tar.gz: 95f7c83226ab47eadea54c08af12a892287b9acc
3
+ metadata.gz: 951694bcd607959a657e046002654767755b74d6
4
+ data.tar.gz: def507a9855268d197362bad03e915720ef2c26f
5
5
  SHA512:
6
- metadata.gz: 49103398cdf8a6469fa599302dcc0c00568ca6014a6f7a7a15c13b9cd8d7768a149ae84626d6399ba51a596294d3077f75ed358cc3476c86059570ac135de255
7
- data.tar.gz: 302868314496ef6b238cae5da06b0516d5d3189629b8ce34071261597b05716f0b06374a3f57b39a4d252a78412d810c40e8ed9c8b4bb8aca809d78e129217ac
6
+ metadata.gz: 0bedc142caf1e81f3aa0dcff437217451f8db5560f6d4ef4f58e3fa6540148ad913e781f28873a20a647e21877c21b0d0397a5877576a1f489d554a82ba6ca54
7
+ data.tar.gz: 6326f21a88d05ce9abf5296e14d5ccd1cf55a9cd57efd9a43628afd82b1afe095fb4ce9562f2f14618ca3562ff4df1f8989036b178fc9d1a2d95a746494cdf6e
@@ -6,6 +6,9 @@ module Blufin
6
6
  autoload :Config, 'core/config'
7
7
  autoload :DateTimeUtils, 'core/datetime_utils'
8
8
  autoload :Files, 'core/files'
9
+ autoload :GenerateBase, 'generate/generate_base'
10
+ autoload :GenerateUiRoutes, 'generate/generate_ui_routes'
11
+ autoload :Git, 'core/git'
9
12
  autoload :Image, 'core/image'
10
13
  autoload :Network, 'core/network'
11
14
  autoload :Numbers, 'core/numbers'
@@ -14,7 +17,9 @@ module Blufin
14
17
  autoload :Strings, 'core/strings'
15
18
  autoload :Terminal, 'core/terminal'
16
19
  autoload :Tools, 'core/tools'
20
+ autoload :Update, 'core/update'
17
21
  autoload :Validate, 'core/validate'
18
22
  autoload :Versioning, 'core/versioning'
23
+ autoload :Yml, 'core/yml'
19
24
 
20
25
  end
@@ -13,6 +13,27 @@ module Blufin
13
13
  !(array.uniq.length == array.length)
14
14
  end
15
15
 
16
+ # Converts an Array of lines to a string (for easier gsub replacement).
17
+ # @return String
18
+ def self.convert_line_array_to_string(array_of_lines)
19
+ raise RuntimeError, "Expected Array of lines, instead got: #{array_of_lines.class}" unless array_of_lines.is_a?(Array)
20
+ string = ''
21
+ array_of_lines.each_with_index do |line, idx|
22
+ newline_or_not = (idx == (array_of_lines.length - 1)) ? '' : "\n"
23
+ string += "#{line}#{newline_or_not}"
24
+ end
25
+ string
26
+ end
27
+
28
+ # Converts a string to an Array of lines to a string.
29
+ # @return String
30
+ def self.convert_string_to_line_array(string)
31
+ raise RuntimeError, "Expected String, instead got: #{string.class}" unless string.is_a?(String)
32
+ array_of_lines = []
33
+ string.split("\n").each { |line| array_of_lines << line.gsub("\n", '') }
34
+ array_of_lines
35
+ end
36
+
16
37
  end
17
38
 
18
39
  end
@@ -4,6 +4,15 @@ module Blufin
4
4
 
5
5
  S3_EXCLUDE = "--exclude '.DS_Store' --exclude '*/.DS_Store' --exclude '*.icloud' --exclude '*/*.icloud'"
6
6
 
7
+ # Checks if script is running on an EC2 instance.
8
+ # Not sure if this is 100% reliable, but it's about as good as it gets I think.
9
+ # @return bool
10
+ def self.is_ec2
11
+ res = `#{Blufin::Base::get_base_path}#{Blufin::Base::OPT_PATH}/shell/ec2-check`.to_s
12
+ res = Blufin::Strings::strip_newline(res)
13
+ res.downcase.strip == 'yes'
14
+ end
15
+
7
16
  # Uploads a file (or path) to S3. If path, will upload recursively (which is mandatory with s3 sync command).
8
17
  # @return string (S3 URL).
9
18
  def self.upload_s3_data(bucket_name, bucket_path, file_or_path, profile: nil, region: nil, is_file: false, dryrun: false)
@@ -25,7 +34,7 @@ module Blufin
25
34
  Blufin::Terminal::info('Performing Dry Run:', Blufin::Terminal::format_command(cmd))
26
35
  system("#{cmd} #{S3_EXCLUDE} --exclude '*.blank*'")
27
36
  else
28
- raise RuntimeError unless Blufin::Terminal::execute("#{cmd} #{S3_EXCLUDE} --exclude '*.blank*'", verbose: true, text: cmd)
37
+ raise RuntimeError unless Blufin::Terminal::execute("#{cmd} #{S3_EXCLUDE} --exclude '*.blank*'", verbose: true, text: cmd)[0]
29
38
  end
30
39
  s3_url
31
40
  rescue
@@ -49,7 +58,7 @@ module Blufin
49
58
  tmp_location = "/tmp/awx-s3-cache-#{bucket_name}#{bucket_path_tmp}"
50
59
  # If path/file exists and we're using cached values, simply return.
51
60
  if file.nil?
52
- return tmp_location if Blufin::Files::path_exists(tmp_location) && use_cache
61
+ return tmp_location if Blufin::Files::path_exists(tmp_location) && use_cache && Blufin::Files::get_files_in_dir(tmp_location).any?
53
62
  else
54
63
  return tmp_location if Blufin::Files::file_exists(tmp_location) && use_cache
55
64
  end
@@ -60,7 +69,7 @@ module Blufin
60
69
  system("rm #{tmp_location}") unless file.nil?
61
70
  Blufin::Files::create_directory(tmp_location)
62
71
  }, verbose: true)
63
- raise RuntimeError unless Blufin::Terminal::execute("aws s3 cp s3://#{bucket_name}#{bucket_path_s3} #{tmp_location} --recursive --region #{region}#{App::AWS::get_profile_for_cli}", verbose: true)
72
+ raise RuntimeError unless Blufin::Terminal::execute("aws s3 cp s3://#{bucket_name}#{bucket_path_s3} #{tmp_location} --recursive --region #{region}#{App::AWS::get_profile_for_cli}", verbose: true)[0]
64
73
  tmp_location
65
74
  rescue
66
75
  system("rm -rf #{tmp_location}") if file.nil?
@@ -56,7 +56,7 @@ module Blufin
56
56
  begin
57
57
  schema_file_parsed = YAML.load_file(schema_file)
58
58
  rescue => e
59
- Blufin::Terminal::error("Failed to parse config file: #{Blufin::Terminal::format_directory(config_file)}", e.message)
59
+ Blufin::Terminal::error("Failed to schema config file: #{Blufin::Terminal::format_directory(schema_file)}", e.message)
60
60
  end
61
61
  validator = Kwalify::Validator.new(schema_file_parsed)
62
62
  begin
@@ -0,0 +1,274 @@
1
+ module Blufin
2
+
3
+ class Git
4
+
5
+ HISTORY_TYPES = %w(branch tag commit)
6
+ DETACHED_HEAD_REGEX = /\(HEAD\s*detached\s*at\s*/
7
+
8
+ @@branch_current_cache = {}
9
+ @@btc_exists_cache = {}
10
+ @@commit_cache = {}
11
+ @@uncommitted_files_cache = {}
12
+
13
+ # Tag a branch:
14
+ # $ git tag -a "v0.1.0-beta" -m "Version: v0.1.0-beta"
15
+
16
+ # Show all commits between 2 tags...
17
+ # $ git log v0.1.0-beta..v0.1.1-beta | grep "^commit [a-z0-9]\{40\}$" | awk '{print $2}'
18
+
19
+ # Get commit message for commit hash... (must .strip string)
20
+ # $ git show d29f2870c59ffab4abb565c3e3c06430cd62515c | grep "^commit [a-z0-9]\{40\}$" -A4 | tail -n 1
21
+
22
+ # Easy, clean way to squash all commits on a branch into 1.
23
+ # $ git reset --soft dev/master
24
+ # $ git add .
25
+ # $ git commit -m 'All changes from my branch squashed'
26
+
27
+ # Merge that single commit into dev (WITHOUT A MERGE COMMIT!).
28
+ # [feature] $ git rebase dev
29
+ # [feature] $ git checkout dev
30
+ # [dev] $ git merge --ff-only feature
31
+
32
+ # Checks out a project and returns the path to it.
33
+ # branch_tag_or_commit can be a branch, commit or tag.
34
+ # Returns current branch/tag/commit (if local) -- in case you want to revert after.
35
+ # @return string |
36
+ def self.checkout(project_id, branch: nil, tag: nil, commit: nil, is_ec2: false)
37
+ begin
38
+ project = Blufin::Projects::get_project_by_id(project_id, true)
39
+ repo_path = Blufin::Projects::get_project_path(project_id, is_ec2: is_ec2)
40
+ repo_data = project[Blufin::Projects::REPOSITORY]
41
+ type, btc = resolve_type_btc(branch, tag, commit)
42
+ errors = nil
43
+ current_head = nil
44
+ if repo_data.has_key?(Blufin::Projects::LOCAL) && !is_ec2
45
+ # Make sure branch/tag/commit exists.
46
+ raise "#{Blufin::Terminal::format_highlight(type.capitalize)} not found: #{Blufin::Terminal::format_invalid(btc)}" unless Blufin::Git::exists(repo_path, btc, type, false)
47
+ # Get current branch (need to reset to this locally after script has run).
48
+ current_head = Blufin::Git::get_current_branch(repo_path)
49
+ else
50
+ # If path already exists, do some checks...
51
+ if Blufin::Files::path_exists(repo_path)
52
+ res = Blufin::Terminal::execute("git remote -v | tail -n 1 | awk '{print $2}'", repo_path, capture: true, verbose: false, display_error: false)
53
+ wipe = false
54
+ res = res[0]
55
+ if res.nil? || Blufin::Strings::strip_newline(res) == ''
56
+ wipe = true
57
+ else
58
+ repo_expected = Blufin::Projects::get_project_repo_name(project_id)
59
+ repo_actual = extract_repo_name(res)
60
+ wipe = true if repo_expected != repo_actual
61
+ end
62
+ # Wipe /tmp folder ONLY if something weird is going on... I put the /tmp regex just in case :)
63
+ `rm -rf #{repo_path}/` if wipe && repo_path =~ /^\/tmp\/[A-Za-z0-9\.]+/
64
+ end
65
+ # Checkout repo (if not exists).
66
+ unless Blufin::Files::path_exists(repo_path)
67
+ clone_cmd = "git clone #{project[Blufin::Projects::REPOSITORY][Blufin::Projects::REMOTE]} #{repo_path}"
68
+ unless Blufin::Terminal::execute_proc(clone_cmd, Proc.new {
69
+ res = Blufin::Terminal::execute("#{clone_cmd} &>/dev/null", '/tmp', capture: true, verbose: false, display_error: false)
70
+ errors = res[1].split("\n")
71
+ res[1] =~ /^Cloning\s*into\s*('|").+/
72
+ })
73
+ raise RuntimeError, "Failed to checkout #{type}: #{Blufin::Terminal::format_invalid(btc)}"
74
+ end
75
+ end
76
+ # At this point we should have the repo checked out. Throw an error if we don't.
77
+ raise RuntimeError, "Path not found: #{repo_path}" unless Blufin::Files::path_exists(repo_path)
78
+ end
79
+ # Checkout branch/tag/commit.
80
+ unless Blufin::Terminal::execute_proc("git checkout #{btc}", Proc.new {
81
+ res = Blufin::Terminal::execute("git checkout #{btc} &>/dev/null", repo_path, capture: true, verbose: false, display_error: false)
82
+ errors = res[1].split("\n")
83
+ last_line = errors[errors.length - 1].strip
84
+ last_line =~ /^HEAD\s*is\s*now\s*at\s*.+/i || last_line =~ /^Already\s*on\s*('|")#{btc}('|")$/ || last_line =~ /^Switched\s*to(\s*a\s*new)?\s*branch\s*('|")#{btc}('|")$/
85
+ })
86
+ raise RuntimeError, "Failed to checkout #{type}: #{Blufin::Terminal::format_invalid(btc)}"
87
+ end
88
+ current_head
89
+ rescue => e
90
+ if is_ec2
91
+ err_output = nil
92
+ err_output = " - #{errors.is_a?(Array) ? errors.join(', ') : errors.to_s}" unless errors.nil? || errors.strip == ''
93
+ raise RuntimeError, Blufin::Strings::strip_ansi_colors("#{e.message}#{err_output}")
94
+ else
95
+ Blufin::Terminal::error(e.message, errors)
96
+ end
97
+ end
98
+ end
99
+
100
+ # Gets current branch for a repository.
101
+ # @return String -> (IE: "master")
102
+ def self.get_current_branch(path = nil, verbose: false)
103
+ raise RuntimeError, "Path not found: #{path}" unless path.nil? || Blufin::Files::path_exists(path)
104
+ path = Blufin::Strings::strip_newline(`pwd`) if path.nil?
105
+ key = File.expand_path(path)
106
+ return @@branch_current_cache[key] if @@branch_current_cache.has_key?(key)
107
+ res = run("git branch | grep \\*", path, text: 'Getting Branch', verbose: verbose)
108
+ branch = Blufin::Strings::strip_newline(res).gsub(/^\*\s?/, '')
109
+ branch = branch.strip.gsub(DETACHED_HEAD_REGEX, '').gsub(/\)$/, '') if branch =~ DETACHED_HEAD_REGEX
110
+ @@branch_current_cache[key] = branch
111
+ @@branch_current_cache[key]
112
+ end
113
+
114
+ # Gets latest commit hash for a repository.
115
+ # @return String -> (IE: "5b7559e5952eacb5251a9baf81dd964fe1ef57f5")
116
+ def self.get_latest_commit_hash(path, verbose: false)
117
+ raise RuntimeError, "Path not found: #{path}" unless Blufin::Files::path_exists(path)
118
+ key = File.expand_path(path)
119
+ return @@commit_cache[key] if @@commit_cache.has_key?(key)
120
+ res = run('git rev-parse HEAD', path, text: 'Getting HEAD Commit', verbose: verbose)
121
+ @@commit_cache[key] = Blufin::Strings::strip_newline(res)
122
+ @@commit_cache[key]
123
+ end
124
+
125
+ # Gets a list of uncommitted files for a repository.
126
+ # Returns an empty Array if branch is clean.
127
+ # @return Array -> (IE: ["file-1.txt","file-2.txt"] or [])
128
+ def self.get_uncommitted_files(path, run_git_add: true, verbose: false, formatted: false, spacer: nil)
129
+ raise RuntimeError, "Path not found: #{path}" unless Blufin::Files::path_exists(path)
130
+ raise RuntimeError, "Expected String, instead got: #{spacer.class}" unless spacer.is_a?(String) || spacer.nil?
131
+ raise RuntimeError, 'Cannot pass spacer if formatted: is false.' if !formatted && !spacer.nil?
132
+ key = "#{File.expand_path(path)}-#{formatted}-#{spacer}"
133
+ return @@uncommitted_files_cache[key] if @@uncommitted_files_cache.has_key?(key)
134
+ renamed = []
135
+ modified = []
136
+ deleted = []
137
+ moved = []
138
+ new = []
139
+ run('git add .', path, verbose: verbose) if run_git_add
140
+ git_status = run('git status', path, verbose: verbose)
141
+ git_status = git_status.split("\n")
142
+ git_status.each do |line|
143
+ renamed << line.split('renamed:')[1].strip if line =~ /renamed:/i
144
+ modified << line.split('modified:')[1].strip if line =~ /modified:/i
145
+ deleted << line.split('deleted:')[1].strip if line =~ /deleted:/i
146
+ moved << line.split('moved:')[1].strip if line =~ /moved:/i
147
+ new << line.split('new file:')[1].strip if line =~ /new file:/i
148
+ end
149
+ if formatted
150
+ files = []
151
+ spacer = '' if spacer.nil?
152
+ new.each { |file| files << "#{spacer}\x1B[38;5;246m#{'New: '.rjust(10, ' ')}\x1B[38;5;48m#{file}\x1B[0m" } if new.any?
153
+ modified.each { |file| files << "#{spacer}\x1B[38;5;246m#{'Modified: '.rjust(10, ' ')}\x1B[38;5;34m#{file}\x1B[0m" } if modified.any?
154
+ renamed.each { |file| files << "#{spacer}\x1B[38;5;246m#{'Renamed: '.rjust(10, ' ')}\x1B[38;5;34m#{file}\x1B[0m" } if renamed.any?
155
+ deleted.each { |file| files << "#{spacer}\x1B[38;5;246m#{'Deleted: '.rjust(10, ' ')}\x1B[38;5;124m#{file}\x1B[0m" } if deleted.any?
156
+ moved.each { |file| files << "#{spacer}\x1B[38;5;246m#{'Moved: '.rjust(10, ' ')}\x1B[38;5;238m#{file}\x1B[0m" } if moved.any?
157
+ else
158
+ files = renamed + modified + deleted + moved + new
159
+ files.sort!
160
+ end
161
+ files.uniq!
162
+ @@uncommitted_files_cache[key] = files
163
+ @@uncommitted_files_cache[key]
164
+ end
165
+
166
+ # Checks if current branch exists.
167
+ # @return bool
168
+ def self.branch_exists(path, branch, run_git_fetch: false)
169
+ exists(path, branch, 'branch', run_git_fetch)
170
+ end
171
+
172
+ # Checks if tag exists.
173
+ # @return bool
174
+ def self.tag_exists(path, tag, run_git_fetch: false)
175
+ exists(path, tag, 'tag', run_git_fetch)
176
+ end
177
+
178
+ # Checks if commit exists.
179
+ # @return bool
180
+ def self.commit_exists(path, commit, run_git_fetch: false)
181
+ exists(path, commit, 'commit', run_git_fetch)
182
+ end
183
+
184
+ # Runs $ git add .
185
+ # @return void
186
+ def self.add(path, add = '.', verbose: true)
187
+ raise RuntimeError, "Path not found: #{path}" unless path.nil? || Blufin::Files::path_exists(path)
188
+ path = Blufin::Strings::strip_newline(`pwd`) if path.nil?
189
+ run("git add #{add}", path, verbose: verbose)
190
+ end
191
+
192
+ # Attempts to convert something like git@github.com:alb3rtuk/blufin-archetypes.git into -> blufin-archetypes.
193
+ # @return string
194
+ def self.extract_repo_name(string)
195
+ raise RuntimeError, "Expected String, instead got: #{string.class}" unless string.is_a?(String)
196
+ repo_name = Blufin::Strings::strip_newline(string).split('/')
197
+ repo_name[repo_name.length - 1].gsub(/\.git$/i, '')
198
+ end
199
+
200
+ private
201
+
202
+ # Convenience method to run commands. Throws error if anything fails.
203
+ # Returns only the output, not the error. Does not strip new lines! This must be done 1 level up.
204
+ # @return string
205
+ def self.run(cmd, path, text: nil, verbose: true)
206
+ res = Blufin::Terminal::execute(cmd, path, capture: true, text: text, verbose: verbose)
207
+ Blufin::Terminal::error("Something went wrong: #{Blufin::Terminal::format_invalid(res[1])}") unless res[1].nil?
208
+ res[0]
209
+ end
210
+
211
+ # Common code for 'exists' method(s).
212
+ # @return bool
213
+ def self.exists(path, btc, type, run_git_fetch)
214
+ raise RuntimeError, "Path not found: #{path}" unless path.nil? || Blufin::Files::path_exists(path)
215
+ raise RuntimeError, "Invalid type: #{type}" unless HISTORY_TYPES.include?(type)
216
+ key = "#{File.expand_path(path)}|#{btc}|#{type}|#{run_git_fetch}"
217
+ return @@btc_exists_cache[key] if @@btc_exists_cache.has_key?(key)
218
+ exists = false
219
+ Blufin::Terminal::execute_proc("Checking #{type} exists: #{Blufin::Terminal::format_highlight(btc)} \x1B[38;5;246m\xe2\x86\x92 \x1B[38;5;240m#{File.expand_path(path)}\x1B[0m", Proc.new {
220
+ run('git fetch -p', path, verbose: false) if run_git_fetch
221
+ cmd = %w(branch tag).include?(type) ? "git #{type} | grep #{btc}" : "git log | grep \"commit #{btc}\""
222
+ res = Blufin::Terminal::execute(cmd, path, capture: true, verbose: false)
223
+ res.each do |line|
224
+ next if line.nil? || line.strip == ''
225
+ case type
226
+ when 'branch'
227
+ line = line.gsub(/^\*\s?/, '')
228
+ when 'commit'
229
+ line = line.gsub(/^commit\s?/, '')
230
+ when 'tag'
231
+ else
232
+ raise RuntimeError, "Unrecognized type: #{type}"
233
+ end
234
+ line = Blufin::Strings::strip_newline(line)
235
+ if line =~ /^#{btc}$/
236
+ exists = true
237
+ break
238
+ end
239
+ end
240
+ exists
241
+ })
242
+ @@btc_exists_cache[key] = exists
243
+ @@btc_exists_cache[key]
244
+ end
245
+
246
+ # Convenience method to resolve type and branch/tag/commit name.
247
+ # @return Array
248
+ def self.resolve_type_btc(branch, tag, commit)
249
+ set = 0
250
+ set =+1 unless branch.nil?
251
+ set =+1 unless tag.nil?
252
+ set =+1 unless commit.nil?
253
+ raise RuntimeError, "Must set atleast one of: #{HISTORY_TYPES.join(', ')}" if set == 0
254
+ raise RuntimeError, "Can only set one of: #{HISTORY_TYPES.join(', ')}" if set > 1
255
+ type = 'branch' unless branch.nil?
256
+ type = 'tag' unless tag.nil?
257
+ type = 'commit' unless commit.nil?
258
+ case type
259
+ when 'branch'
260
+ btc = branch
261
+ when 'tag'
262
+ btc = tag
263
+ when 'commit'
264
+ btc = commit
265
+ else
266
+ raise RuntimeError, "Unrecognized type: #{type}"
267
+ end
268
+ raise RuntimeError, "#{type.capitalize} cannot be nil." if btc.nil? || btc.strip == ''
269
+ [type, btc]
270
+ end
271
+
272
+ end
273
+
274
+ end
@@ -16,7 +16,10 @@ module Blufin
16
16
  PROJECT_ROOT = 'ProjectRoot'
17
17
  TYPE = 'Type'
18
18
  REPOSITORY = 'Repository'
19
+ UPSTREAM = 'Upstream'
20
+ DOWNSTREAM = 'Downstream'
19
21
  LOCAL = 'Local'
22
+ REMOTE = 'Remote'
20
23
  FILE = 'File'
21
24
  DEPLOYMENT = 'Deployment'
22
25
  DEPLOYMENT_BUCKET = 'Bucket'
@@ -29,6 +32,8 @@ module Blufin
29
32
  CRON = 'Cron'
30
33
  WORKER = 'Worker'
31
34
  LAMBDA = 'Lambda'
35
+ UI = 'UI'
36
+ ROUTES_FILE = 'RoutesFile'
32
37
  TITLE = 'Title'
33
38
  ALIAS = 'Alias'
34
39
  DOMAIN = 'Domain'
@@ -40,25 +45,40 @@ module Blufin
40
45
  TYPE_ALEXA = 'alexa'
41
46
  TYPE_API = 'api'
42
47
  TYPE_LAMBDA = 'lambda'
48
+ TYPE_MVN_LIB = 'mvn-lib'
49
+ TYPE_NPM_LIB = 'npm-lib'
43
50
  TYPE_UI = 'ui'
51
+ TYPE_MOBILE = 'mobile'
44
52
  VALID_TYPES = [
45
53
  TYPE_ALEXA,
46
54
  TYPE_API,
47
55
  TYPE_LAMBDA,
56
+ TYPE_MVN_LIB,
57
+ TYPE_NPM_LIB,
48
58
  TYPE_UI,
59
+ TYPE_MOBILE,
49
60
  ]
50
61
 
51
- @@projects = nil
52
- @@project_names = []
53
- @@scripts = nil
54
- @@apis = nil
55
- @@lambdas = nil
62
+ @@projects = nil
63
+ @@projects_arr = []
64
+ @@projects_cache = {}
65
+ @@project_names = []
66
+ @@project_ids = []
67
+ @@scripts = nil
68
+ @@apis = nil
69
+ @@api_data = {}
70
+ @@lambdas = nil
71
+ @@libs = {}
72
+ @@uis = nil
73
+ @@dependant_projects_cache = {}
74
+ @@dependant_repos_cache = {}
75
+ @@project_path_cache = {}
56
76
 
57
77
  # Takes a Hash that needs to have a 'Projects' key.
58
78
  # This can come from both .awx.yml['Profiles'] or .blufin.yml (root).
59
79
  # @return void
60
- def self.init(projects)
61
- raise RuntimeError, 'Cannot run Blufin::Projects::init() more than once.' unless @@projects.nil? && @@scripts.nil?
80
+ def initialize(projects)
81
+ raise RuntimeError, 'Cannot run Blufin::Projects.new() more than once.' if !@@projects.nil? || !@@scripts.nil?
62
82
  raise RuntimeError, "Need either a Local or S3Bucket key, found neither: #{projects.keys}" unless projects.has_key?(LOCAL) || projects.has_key?('S3Bucket')
63
83
  @@projects = {}
64
84
  @@scripts = {}
@@ -77,20 +97,40 @@ module Blufin
77
97
  raise RuntimeError, 'Reading projects.yml from S3 is not yet implemented!'
78
98
 
79
99
  end
100
+
80
101
  end
81
102
 
82
- # Gets Project(s).
103
+ # Gets Project(s) -- as a nested hash with -> [PROJECT][PROJECT_ID] as the keys.
83
104
  # @return Hash
84
105
  def self.get_projects
85
106
  @@projects
86
107
  end
87
108
 
109
+ # Gets Project(s) -- but in a single array.
110
+ # @return Array
111
+ def self.get_projects_as_array(type: nil)
112
+ projects_arr = []
113
+ if type.nil?
114
+ validate_type(project_type, project_id)
115
+ projects_arr = @@projects_arr
116
+ else
117
+ @@projects_arr.each { |project| projects_arr << project if project[TYPE] == type }
118
+ end
119
+ projects_arr
120
+ end
121
+
88
122
  # Gets Project Name(s).
89
123
  # @return Array
90
124
  def self.get_project_names
91
125
  @@project_names
92
126
  end
93
127
 
128
+ # Gets Project ID(s).
129
+ # @return Array
130
+ def self.get_project_ids
131
+ @@project_ids
132
+ end
133
+
94
134
  # Gets Script(s).
95
135
  # @return Hash
96
136
  def self.get_scripts
@@ -109,25 +149,147 @@ module Blufin
109
149
  @@lambdas
110
150
  end
111
151
 
112
- # Maps root-level property to enum.
152
+ # Attempts to get a project's data by ID, displays error if not exists.
113
153
  # @return string
114
- def self.script_key_mapper(script_type)
115
- if [RUN_SCRIPTS, RUN].include?(script_type)
116
- return SCRIPT_RUN
117
- elsif [TEST_SCRIPTS, TEST].include?(script_type)
118
- return TEST_SCRIPTS
119
- elsif [BUILD_SCRIPTS, BUILD].include?(script_type)
120
- return BUILD_SCRIPTS
154
+ def self.get_project_by_id(project_id, runtime_error = false)
155
+ raise RuntimeError, "Expected String, instead got: #{project_id.class}" unless project_id.is_a?(String)
156
+ return @@projects_cache[project_id] if @@projects_cache.has_key?(project_id)
157
+ project_data = nil
158
+ if @@projects_arr.is_a?(Array)
159
+ @@projects_arr.each do |project|
160
+ raise RuntimeError, 'Missing Project ID.' unless project.has_key?(PROJECT_ID)
161
+ if project[PROJECT_ID].strip.downcase == project_id.strip.downcase
162
+ project_data = project
163
+ break
164
+ end
165
+ end
166
+ end
167
+ raise RuntimeError, "Unrecognized Project ID: #{project_id}" if project_data.nil? && runtime_error
168
+ Blufin::Terminal::error("Unrecognized Project ID: #{Blufin::Terminal::format_invalid(project_id)} . Available Projects IDs are:", get_project_ids) if project_data.nil? && !runtime_error
169
+ @@projects_cache[project_id] = project_data
170
+ @@projects_cache[project_id]
171
+ end
172
+
173
+ # Gets an array of project(s) from current path.
174
+ # @return Array
175
+ def self.get_projects_by_path
176
+ projects = {}
177
+ current_path = Blufin::Strings::strip_newline(`pwd`)
178
+ get_projects_as_array.each do |project|
179
+ if project.has_key?(REPOSITORY)
180
+ if project[REPOSITORY].has_key?(LOCAL)
181
+ project_path = File.expand_path(project[REPOSITORY][LOCAL])
182
+ if current_path =~ /^#{project_path}/
183
+ raise RuntimeError, 'Missing Project ID.' unless project.has_key?(PROJECT_ID)
184
+ projects[project[PROJECT_ID]] = project
185
+ end
186
+ end
187
+ end
188
+ end
189
+ projects
190
+ end
191
+
192
+ # Gets the path to a project. By default, gets the root (IE: doesn't take into account 'Repository.ProjectRoot' key).
193
+ # If you want the full path to the inner project, to_inner_project must be set to TRUE.
194
+ # If local, simply returns path in projects.yml.
195
+ # If not local (IE: on EC2), returns a standardized /tmp path.
196
+ # @return string
197
+ def self.get_project_path(project_id, to_inner_project = false, is_ec2: false, project: nil)
198
+ key = "#{project_id}|#{to_inner_project}|#{is_ec2}"
199
+ return @@project_path_cache[key] if @@project_path_cache.has_key?(key)
200
+ project = project.nil? ? get_project_by_id(project_id, true) : project
201
+ repo_data = project[REPOSITORY]
202
+ inner_path = repo_data.has_key?(PROJECT_ROOT) ? Blufin::Strings::remove_surrounding_slashes(repo_data[PROJECT_ROOT]) : ''
203
+ project_path = nil
204
+ if repo_data.has_key?(LOCAL) && !is_ec2
205
+ root_path = Blufin::Strings::remove_surrounding_slashes(File.expand_path(repo_data[LOCAL]))
206
+ project_path = "/#{root_path}" unless to_inner_project
207
+ project_path = "/#{root_path}#{inner_path.length > 0 ? '/' : ''}#{inner_path}" if to_inner_project
121
208
  else
122
- raise RuntimeError, "Unhandled script type: #{script_type}"
209
+ rs = repo_data[REMOTE].split('/')
210
+ tmp_path = "/tmp/repo-#{rs[rs.length - 1].gsub(/\.git$/i, '')}"
211
+ project_path = tmp_path unless to_inner_project
212
+ project_path = "#{tmp_path}#{inner_path.length > 0 ? '/' : ''}#{inner_path}" if to_inner_project
213
+ end
214
+ raise RuntimeError, "Project Path should never be nil or an empty string: #{key}" if project_path.nil? || project_path.strip == ''
215
+ @@project_path_cache[key] = project_path
216
+ @@project_path_cache[key]
217
+ end
218
+
219
+ # Gets a hash of dependant projects (with the Project IDs as keys).
220
+ # processed_projects Array prevents cyclic-dependency stack overflow.
221
+ # @return Hash
222
+ def self.get_dependant_projects(project_id, streams: [UPSTREAM, DOWNSTREAM], processed_projects: [])
223
+ valid_streams = [UPSTREAM, DOWNSTREAM]
224
+ raise RuntimeError, "Expected Array, instead got: #{streams.class}" unless streams.is_a?(Array)
225
+ streams.each { |s| raise RuntimeError, "Invalid stream: #{s}" unless valid_streams.include?(s) }
226
+ key = "#{project_id}-#{streams.join('-')}"
227
+ return @@dependant_projects_cache[key] if @@dependant_projects_cache.has_key?(key)
228
+ dependant_projects = {}
229
+ project = get_project_by_id(project_id, true)
230
+ streams.each do |stream|
231
+ if project.has_key?(stream)
232
+ project[stream].each do |dependant_project_id|
233
+ unless processed_projects.include?(dependant_project_id)
234
+ processed_projects << dependant_project_id
235
+ dependant_projects[dependant_project_id] = get_project_by_id(dependant_project_id, true) unless dependant_projects.has_key?(dependant_project_id)
236
+ dependant_projects_inner = Blufin::Projects::get_dependant_projects(dependant_project_id, streams: streams, processed_projects: processed_projects)
237
+ # Add nested dependant projects (if any).
238
+ dependant_projects_inner.each { |k, v| dependant_projects[k] = v unless dependant_projects.has_key?(k) } if dependant_projects_inner.any?
239
+ end
240
+ end
241
+ end
123
242
  end
243
+ # Don't include project itself in dependant projects.
244
+ dependant_projects.delete(project_id) if dependant_projects.has_key?(project_id)
245
+ @@dependant_projects_cache[key] = dependant_projects
246
+ @@dependant_projects_cache[key]
247
+ end
248
+
249
+ # Same as above, but only gets the repositories.
250
+ # Sometimes multiple dependant_project IDs may have the same repository.
251
+ # @return Hash
252
+ def self.get_dependant_repos(project_id, streams: [UPSTREAM, DOWNSTREAM])
253
+ key = "#{project_id}-#{streams.join('-')}"
254
+ return @@dependant_repos_cache[key] if @@dependant_repos_cache.has_key?(key)
255
+ dependant_repos = {}
256
+ get_dependant_projects(project_id, streams: streams).each do |k, v|
257
+ next if k == project_id
258
+ repo = v[REPOSITORY][REMOTE]
259
+ dependant_repos[repo] = {
260
+ :projects => []
261
+ } unless dependant_repos.include?(repo)
262
+ dependant_repos[repo][:path] = v[REPOSITORY][LOCAL] if v[REPOSITORY].has_key?(LOCAL)
263
+ dependant_repos[repo][:projects] << k unless dependant_repos[repo][:projects].include?(k)
264
+ end
265
+ @@dependant_repos_cache[key] = dependant_repos
266
+ @@dependant_repos_cache[key]
267
+ end
268
+
269
+ # Gets repo-name from project_id.
270
+ # @return string
271
+ def self.get_project_repo_name(project_id)
272
+ project = get_project_by_id(project_id, true)
273
+ Blufin::Git::extract_repo_name(project[REPOSITORY][REMOTE])
274
+ end
275
+
276
+ # Shows a prompt and returns project Hash once selected.
277
+ # If only one project exists, prompt not displayed.
278
+ # @return Hash
279
+ def self.show_project_prompt(array_of_projects)
280
+ raise RuntimeError, "Expected Array, instead got: #{array_of_projects.class}" unless array_of_projects.is_a?(Array)
281
+ Blufin::Terminal::error('No projects found.') unless array_of_projects.any?
282
+ return array_of_projects[0] if array_of_projects.length == 1
283
+ projects = []
284
+ array_of_projects.each { |project| projects << { :text => project[PROJECT_ID], :value => project } }
285
+ Blufin::Terminal::prompt_select('Select project:', projects)
124
286
  end
125
287
 
126
288
  private
127
289
 
128
290
  # Validate the Project YML.
129
291
  # @return void
130
- def self.process_source_file(source_file)
292
+ def process_source_file(source_file)
131
293
  # Skip empty file.
132
294
  return if Blufin::Files::is_empty(source_file)
133
295
  # Otherwise, validate file.
@@ -170,8 +332,31 @@ module Blufin
170
332
  end
171
333
  end
172
334
 
335
+ # Buffer/validate project(s) stuff.
336
+ file_parsed['Projects'].each do |project|
337
+ project_id = project[PROJECT_ID]
338
+ project_type = project[TYPE]
339
+ if project_type == TYPE_MVN_LIB || TYPE_NPM_LIB
340
+ Blufin::Terminal::error("#{Blufin::Terminal::format_highlight(project_id)} \xe2\x80\x94 Duplicate Library Project.", "A library with this ID (#{Blufin::Terminal::format_invalid(project_id)}) has already been registered.") if @@libs.keys.include?(project_id)
341
+ @@libs[project_id] = project
342
+ elsif project_type == TYPE_API
343
+ [ALIAS, PROJECT_NAME, PROJECT_NAME_PASCAL_CASE, TITLE].each do |x|
344
+ @@api_data[x] = [] unless @@api_data.has_key?(x) && @@api_data[x].is_a?(Array)
345
+ property_value = project[API][x].strip.downcase
346
+ Blufin::Terminal::error("#{Blufin::Terminal::format_highlight(project_id)} \xe2\x80\x94 Duplicate #{x} API Property.", "An API Property with this value (#{Blufin::Terminal::format_invalid(property_value)}) has already been registered.") if @@api_data[x].include?(property_value)
347
+ @@api_data[x] << property_value
348
+ end
349
+ end
350
+ end
351
+
173
352
  used_ports = {}
174
353
 
354
+ # Run through once quickly to populate critical objects (required for validation).
355
+ file_parsed['Projects'].each do |project|
356
+ @@project_names << project[PROJECT]
357
+ @@project_ids << project[PROJECT_ID]
358
+ end
359
+
175
360
  # Loop (and validate) projects.
176
361
  file_parsed['Projects'].each do |project|
177
362
  # Validate keys are in specific order.
@@ -180,25 +365,26 @@ module Blufin
180
365
  PROJECT => true,
181
366
  TYPE => true,
182
367
  REPOSITORY => true,
368
+ UPSTREAM => false,
369
+ DOWNSTREAM => false,
183
370
  RUN => false,
184
371
  TEST => false,
185
372
  BUILD => false,
186
373
  API => false,
187
374
  LAMBDA => false,
188
- DEPLOYMENT => false
375
+ UI => false,
376
+ DEPLOYMENT => false,
189
377
  }
190
378
  Blufin::Validate::assert_valid_keys(expected, project.keys, source_file)
191
379
  project_id = project[PROJECT_ID]
192
380
  project_name = project[PROJECT]
193
381
  project_type = project[TYPE]
194
- @@project_names << project_name
195
- # Validate Type.
196
- Blufin::Terminal::error("#{project_id} \xe2\x80\x94 Invalid Project Type: #{Blufin::Terminal::format_invalid(project_type)}. Valid types are:", VALID_TYPES, true) unless VALID_TYPES.include?(project_type)
382
+ validate_type(project_type, project_id)
197
383
  # Validate Script(s).
198
384
  [RUN, TEST, BUILD].each do |script_type|
199
385
  if project.has_key?(script_type)
200
386
  # Validate the LAMBDA functions don't need build scripts.
201
- Blufin::Terminal::error("#{project_id} \xe2\x80\x94 Project type: #{Blufin::Terminal::format_highlight(TYPE_LAMBDA)} does not require #{Blufin::Terminal::format_invalid(script_type)} script(s).", 'This type of project does not support this.', true) if [BUILD].include?(script_type) && project_type == TYPE_LAMBDA
387
+ Blufin::Terminal::error("#{Blufin::Terminal::format_highlight(project_id)} \xe2\x80\x94 Project type: #{Blufin::Terminal::format_highlight(TYPE_LAMBDA)} does not require #{Blufin::Terminal::format_invalid(script_type)} script(s).", 'This type of project does not support this.', true) if [BUILD].include?(script_type) && project_type == TYPE_LAMBDA
202
388
  if project[script_type].is_a?(Hash)
203
389
  script_key = script_key_mapper(script_type)
204
390
  valid_scripts = []
@@ -207,12 +393,21 @@ module Blufin
207
393
  script_name = script['Script']
208
394
  unless valid_scripts.include?(script_name)
209
395
  error = valid_scripts.any? ? 'Valid values are:' : "There currently are no #{script_key} script(s) defined."
210
- Blufin::Terminal::error("#{project_id} \xe2\x80\x94 #{Blufin::Terminal::format_highlight(script_type)} \xe2\x80\x94 Invalid script reference: #{Blufin::Terminal::format_invalid(script_name)}. #{error}", valid_scripts)
396
+ Blufin::Terminal::error("#{Blufin::Terminal::format_highlight(project_id)} \xe2\x80\x94 #{Blufin::Terminal::format_highlight(script_type)} \xe2\x80\x94 Invalid script reference: #{Blufin::Terminal::format_invalid(script_name)}. #{error}", valid_scripts)
211
397
  end
212
398
  end
213
399
  end
214
400
  end
215
401
 
402
+ # Validate Repository property.
403
+ if project.has_key?(REPOSITORY)
404
+ if project[REPOSITORY].has_key?(LOCAL)
405
+ repo_path = project[REPOSITORY][LOCAL]
406
+ Blufin::Terminal::error("#{Blufin::Terminal::format_highlight(project_id)} \xe2\x80\x94 Repository path not found: #{Blufin::Terminal::format_invalid(repo_path)}") unless Blufin::Files::path_exists(repo_path)
407
+
408
+ end
409
+ end
410
+
216
411
  # Validate Deployment property.
217
412
  if project.has_key?(DEPLOYMENT)
218
413
  expected = {
@@ -225,7 +420,7 @@ module Blufin
225
420
  # Validate API property.
226
421
  if project_type == TYPE_API
227
422
  # Make sure we have the API property.
228
- Blufin::Terminal::error("#{project_id} \xe2\x80\x94 Missing property: #{Blufin::Terminal::format_highlight(API)}", "This property is required for project(s) with type: #{API}", true) unless project.has_key?(API)
423
+ Blufin::Terminal::error("#{Blufin::Terminal::format_highlight(project_id)} \xe2\x80\x94 Missing property: #{Blufin::Terminal::format_highlight(API)}", "This property is required for project(s) with type: #{API}", true) unless project.has_key?(API)
229
424
  # Validate keys are in specific order.
230
425
  expected = {
231
426
  TITLE => true,
@@ -248,16 +443,16 @@ module Blufin
248
443
  used_ports[project[API][PORTS][CRON]] = project_id
249
444
  used_ports[project[API][PORTS][WORKER]] = project_id
250
445
  @@apis = {} if @@apis.nil?
251
- @@apis[project[PROJECT_ID]] = project[API]
446
+ @@apis[project[PROJECT_ID]] = project
252
447
  else
253
448
  # Make sure we DON'T have the API key.
254
- Blufin::Terminal::error("#{project_id} \xe2\x80\x94 Property not supported: #{Blufin::Terminal::format_invalid(API)}", "This property is only allowed for project(s) with type: #{API}", true) if project.has_key?(API)
449
+ Blufin::Terminal::error("#{Blufin::Terminal::format_highlight(project_id)} \xe2\x80\x94 Property not supported: #{Blufin::Terminal::format_invalid(API)}", "This property is only allowed for project(s) with type: #{API}", true) if project.has_key?(API)
255
450
  end
256
451
 
257
452
  # Validate Lambda property.
258
453
  if project_type == TYPE_LAMBDA
259
- # Make sure we have the Lambda
260
- Blufin::Terminal::error("#{project_id} \xe2\x80\x94 Missing property: #{Blufin::Terminal::format_highlight(LAMBDA)}", "This property is required for project(s) with type: #{LAMBDA}", true) unless project.has_key?(LAMBDA)
454
+ # Make sure we have the Lambda property.
455
+ Blufin::Terminal::error("#{Blufin::Terminal::format_highlight(project_id)} \xe2\x80\x94 Missing property: #{Blufin::Terminal::format_highlight(LAMBDA)}", "This property is required for project(s) with type: #{TYPE_LAMBDA}", true) unless project.has_key?(LAMBDA)
261
456
  # Validate keys are in specific order.
262
457
  expected = {
263
458
  TITLE => true,
@@ -274,12 +469,49 @@ module Blufin
274
469
  @@lambdas[project[PROJECT_ID]] = project
275
470
  else
276
471
  # Make sure we DON'T have the Lambda key.
277
- Blufin::Terminal::error("#{project_id} \xe2\x80\x94 Property not supported: #{Blufin::Terminal::format_invalid(LAMBDA)}", "This property is only allowed for project(s) with type: #{LAMBDA}", true) if project.has_key?(LAMBDA)
472
+ Blufin::Terminal::error("#{Blufin::Terminal::format_highlight(project_id)} \xe2\x80\x94 Property not supported: #{Blufin::Terminal::format_invalid(LAMBDA)}", "This property is only allowed for project(s) with type: #{TYPE_LAMBDA}", true) if project.has_key?(LAMBDA)
473
+ end
474
+
475
+ # Validate UI property.
476
+ if project_type == TYPE_UI
477
+ # Make sure we have the UI property.
478
+ Blufin::Terminal::error("#{Blufin::Terminal::format_highlight(project_id)} \xe2\x80\x94 Missing property: #{Blufin::Terminal::format_highlight(UI)}", "This property is required for project(s) with type: #{TYPE_UI}", true) unless project.has_key?(UI)
479
+ # Validate keys are in specific order.
480
+ expected = {
481
+ ROUTES_FILE => true
482
+ }
483
+ Blufin::Validate::assert_valid_keys(expected, project[UI].keys, source_file)
484
+ # Validate RoutesFile exists.
485
+ routes_file = "#{Blufin::Projects::get_project_path(project_id, true, project: project)}/#{project[UI][ROUTES_FILE]}"
486
+ Blufin::Terminal::error("Cannot find #{Blufin::Terminal::format_highlight(ROUTES_FILE)}: #{Blufin::Terminal::format_directory(routes_file)}") unless Blufin::Files::file_exists(routes_file)
487
+ @@uis = {} if @@uis.nil?
488
+ Blufin::Terminal::error("Duplicate UI project: #{Blufin::Terminal::format_invalid(project[PROJECT_ID])}") if @@uis.has_key?(project[PROJECT_ID])
489
+ @@uis[project[PROJECT_ID]] = project
490
+ else
491
+ # Make sure we DON'T have the UI key.
492
+ Blufin::Terminal::error("#{Blufin::Terminal::format_highlight(project_id)} \xe2\x80\x94 Property not supported: #{Blufin::Terminal::format_invalid(UI)}", "This property is only allowed for project(s) with type: #{TYPE_UI}", true) if project.has_key?(UI)
493
+ end
494
+
495
+ # Validate upstream/downstream Libs(s).
496
+ [UPSTREAM, DOWNSTREAM].each do |stream|
497
+ if project.has_key?(stream)
498
+ project[stream].each do |library|
499
+ case stream
500
+ when UPSTREAM
501
+ Blufin::Terminal::error("#{Blufin::Terminal::format_highlight(project_id)} \xe2\x80\x94 Unrecognized #{Blufin::Terminal::format_action(UPSTREAM)} library: #{Blufin::Terminal::format_invalid(library)}") unless @@libs.keys.include?(library)
502
+ when DOWNSTREAM
503
+ Blufin::Terminal::error("#{Blufin::Terminal::format_highlight(project_id)} \xe2\x80\x94 Unrecognized #{Blufin::Terminal::format_action(DOWNSTREAM)} library: #{Blufin::Terminal::format_invalid(library)}") unless @@project_ids.include?(library)
504
+ else
505
+ raise RuntimeError, "Unrecognized stream: #{stream}"
506
+ end
507
+ end
508
+ end
278
509
  end
279
510
 
280
511
  @@projects[project_name] = {} unless @@projects.has_key?(project_name)
281
512
  Blufin::Terminal::error("Duplicate project ID: #{Blufin::Terminal::format_invalid(project_id)}") if @@projects[project_name].has_key?(project_id)
282
513
  @@projects[project_name][project_id] = project
514
+ @@projects_arr << project
283
515
 
284
516
  end
285
517
  @@project_names.uniq!
@@ -287,12 +519,33 @@ module Blufin
287
519
  end
288
520
  end
289
521
 
522
+ # Validate project type.
523
+ # @return void
524
+ def validate_type(project_type, project_id = nil)
525
+ project_id = project_id.nil? ? nil : "#{Blufin::Terminal::format_highlight(project_id)} \xe2\x80\x94 "
526
+ Blufin::Terminal::error("#{project_id}Invalid Project Type: #{Blufin::Terminal::format_invalid(project_type)}. Valid types are:", VALID_TYPES, true) unless VALID_TYPES.include?(project_type)
527
+ end
528
+
529
+ # Maps root-level property to enum.
530
+ # @return string
531
+ def script_key_mapper(script_type)
532
+ if [RUN_SCRIPTS, RUN].include?(script_type)
533
+ return SCRIPT_RUN
534
+ elsif [TEST_SCRIPTS, TEST].include?(script_type)
535
+ return TEST_SCRIPTS
536
+ elsif [BUILD_SCRIPTS, BUILD].include?(script_type)
537
+ return BUILD_SCRIPTS
538
+ else
539
+ raise RuntimeError, "Unhandled script type: #{script_type}"
540
+ end
541
+ end
542
+
290
543
  # Validate the ports. Make sure none of the ports conflict with other projects.
291
544
  # @return void
292
- def self.validate_ports(ports, project_id, used_ports)
545
+ def validate_ports(ports, project_id, used_ports)
293
546
  ports.each do |port|
294
547
  if used_ports.has_key?(port)
295
- Blufin::Terminal::error("#{project_id} \xe2\x80\x94 Duplicate port detected: #{Blufin::Terminal::format_invalid(port)} ", ["The conflicting project is: #{Blufin::Terminal::format_highlight(used_ports[port])}"])
548
+ Blufin::Terminal::error("#{Blufin::Terminal::format_highlight(project_id)} \xe2\x80\x94 Duplicate port detected: #{Blufin::Terminal::format_invalid(port)} ", ["The conflicting project is: #{Blufin::Terminal::format_highlight(used_ports[port])}"])
296
549
  end
297
550
 
298
551
  end