ace-support-config 0.10.2 â 0.16.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +106 -0
- data/README.md +16 -1
- data/docs/demo/ace-config-bootstrap-root-files.tape.yml +31 -0
- data/docs/demo/ace-config-getting-started.tape.yml +3 -3
- data/docs/usage.md +52 -5
- data/lib/ace/support/config/cli.rb +56 -12
- data/lib/ace/support/config/molecules/project_config_scanner.rb +1 -1
- data/lib/ace/support/config/molecules/setup_doctor_reporter.rb +270 -0
- data/lib/ace/support/config/organisms/config_synchronizer.rb +197 -0
- data/lib/ace/support/config/organisms/setup_doctor.rb +1069 -0
- data/lib/ace/support/config/version.rb +1 -1
- data/lib/ace/support/config.rb +3 -1
- metadata +6 -3
- data/lib/ace/support/config/organisms/config_initializer.rb +0 -116
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Support
|
|
7
|
+
module Config
|
|
8
|
+
module Molecules
|
|
9
|
+
class SetupDoctorReporter
|
|
10
|
+
COLORS = {
|
|
11
|
+
red: "\e[31m",
|
|
12
|
+
yellow: "\e[33m",
|
|
13
|
+
green: "\e[32m",
|
|
14
|
+
blue: "\e[34m",
|
|
15
|
+
cyan: "\e[36m",
|
|
16
|
+
reset: "\e[0m",
|
|
17
|
+
bold: "\e[1m"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
ICONS = {
|
|
21
|
+
doctor: "đĨ",
|
|
22
|
+
stats: "đ",
|
|
23
|
+
success: "â
",
|
|
24
|
+
error: "â",
|
|
25
|
+
warning: "â ī¸",
|
|
26
|
+
info: "âšī¸"
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
STATUS_GLYPHS = {
|
|
30
|
+
"pass" => "â",
|
|
31
|
+
"warn" => "â",
|
|
32
|
+
"blocker" => "â",
|
|
33
|
+
"skip" => "â",
|
|
34
|
+
"info" => "â"
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
STATUS_COLORS = {
|
|
38
|
+
"pass" => :green,
|
|
39
|
+
"warn" => :yellow,
|
|
40
|
+
"blocker" => :red,
|
|
41
|
+
"skip" => :yellow,
|
|
42
|
+
"info" => :cyan
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
def self.format_results(results, format: :terminal, hygiene: false, verbose: false, colors: true)
|
|
46
|
+
case format.to_sym
|
|
47
|
+
when :json
|
|
48
|
+
JSON.pretty_generate(results)
|
|
49
|
+
else
|
|
50
|
+
new(results, hygiene: hygiene, verbose: verbose, colors: colors).format_terminal
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def initialize(results, hygiene:, verbose:, colors:)
|
|
55
|
+
@results = results
|
|
56
|
+
@hygiene = hygiene
|
|
57
|
+
@verbose = verbose
|
|
58
|
+
@colors = colors
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def format_terminal
|
|
62
|
+
output = []
|
|
63
|
+
output << "\n#{colorize("#{ICONS[:doctor]} Setup Health Check", :bold)}"
|
|
64
|
+
output << "=" * 40
|
|
65
|
+
output.concat(format_overview)
|
|
66
|
+
output.concat(format_readiness)
|
|
67
|
+
output.concat(format_info)
|
|
68
|
+
output.concat(format_provider_pings)
|
|
69
|
+
output.concat(format_issues)
|
|
70
|
+
output << "=" * 40
|
|
71
|
+
output << format_summary_line
|
|
72
|
+
output << "\n#{colorize("Completed in #{format_duration(@results[:duration])}", :blue)}" if @results[:duration]
|
|
73
|
+
output << final_status_line
|
|
74
|
+
output.join("\n")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def format_overview
|
|
80
|
+
stats = @results[:stats] || {}
|
|
81
|
+
[
|
|
82
|
+
"",
|
|
83
|
+
colorize("#{ICONS[:stats]} Overview", :cyan),
|
|
84
|
+
"-" * 20,
|
|
85
|
+
" Health checks: #{stats[:health_checks] || health_checks.length}",
|
|
86
|
+
" Info checks: #{stats[:info_checks] || info_checks.length}",
|
|
87
|
+
" Utility providers checked: #{stats[:provider_targets] || provider_outcomes.length}",
|
|
88
|
+
" Hygiene findings: #{stats[:hygiene_findings] || 0}"
|
|
89
|
+
]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def format_readiness
|
|
93
|
+
rows = health_checks.reject { |check| check[:id] == "probe-readiness" }
|
|
94
|
+
return [] if rows.empty?
|
|
95
|
+
|
|
96
|
+
output = ["", colorize("Readiness", :cyan), "-" * 20]
|
|
97
|
+
rows.each do |check|
|
|
98
|
+
output << " #{status_glyph(check[:status])} #{check[:message]}"
|
|
99
|
+
if check[:status] == "blocker" || check[:status] == "warn" || @verbose
|
|
100
|
+
Array(check[:details]).each { |detail| output << " - #{detail}" }
|
|
101
|
+
output << " Next: #{check[:next_action]}" if check[:next_action]
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
output
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def format_info
|
|
108
|
+
return [] if info_checks.empty?
|
|
109
|
+
|
|
110
|
+
output = ["", colorize("Info", :cyan), "-" * 20]
|
|
111
|
+
info_checks.each do |check|
|
|
112
|
+
output << " #{status_glyph(check[:status])} #{check[:message]}"
|
|
113
|
+
if @verbose
|
|
114
|
+
Array(check[:details]).each { |detail| output << " - #{detail}" }
|
|
115
|
+
output << " Next: #{check[:next_action]}" if check[:next_action]
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
output
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def format_provider_pings
|
|
122
|
+
check = health_checks.find { |row| row[:id] == "probe-readiness" }
|
|
123
|
+
return [] unless check
|
|
124
|
+
|
|
125
|
+
output = ["", colorize("Utility Provider Pings", :cyan), "-" * 20]
|
|
126
|
+
output << " #{status_glyph(check[:status])} #{check[:message]}"
|
|
127
|
+
details = provider_outcomes.any? ? provider_outcomes.map { |outcome| provider_outcome_line(outcome) } : Array(check[:details])
|
|
128
|
+
details.each { |detail| output << " #{detail}" }
|
|
129
|
+
output << " Next: #{check[:next_action]}" if check[:next_action]
|
|
130
|
+
output
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def format_issues
|
|
134
|
+
blockers = health_checks.select { |check| check[:status] == "blocker" }
|
|
135
|
+
warnings = health_checks.select { |check| check[:status] == "warn" }
|
|
136
|
+
hygiene_warnings = hygiene_checks.reject { |check| check[:status] == "pass" }
|
|
137
|
+
|
|
138
|
+
if blockers.empty? && warnings.empty? && hygiene_warnings.empty?
|
|
139
|
+
return ["", colorize("#{ICONS[:success]} No issues found", :green)]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
output = ["", colorize("Issues Found:", :yellow), "-" * 20]
|
|
143
|
+
output.concat(format_issue_group("#{ICONS[:error]} Blockers", blockers, :red, include_details: true))
|
|
144
|
+
output.concat(format_issue_group("#{ICONS[:warning]} Warnings", warnings, :yellow, include_details: true))
|
|
145
|
+
output.concat(format_hygiene_group(hygiene_warnings))
|
|
146
|
+
output
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def format_issue_group(title, checks, color, include_details:)
|
|
150
|
+
return [] if checks.empty?
|
|
151
|
+
|
|
152
|
+
output = ["", colorize("#{title} (#{checks.length})", color)]
|
|
153
|
+
checks.each_with_index do |check, index|
|
|
154
|
+
output << "#{index + 1}. #{check[:message]}"
|
|
155
|
+
if include_details
|
|
156
|
+
Array(check[:details]).each { |detail| output << " - #{detail}" }
|
|
157
|
+
end
|
|
158
|
+
output << " Next: #{check[:next_action]}" if check[:next_action]
|
|
159
|
+
end
|
|
160
|
+
output
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def format_hygiene_group(checks)
|
|
164
|
+
finding_count = @results.dig(:hygiene, :finding_count) || 0
|
|
165
|
+
return [] if finding_count.zero?
|
|
166
|
+
|
|
167
|
+
if !@hygiene && !@verbose
|
|
168
|
+
return [
|
|
169
|
+
"",
|
|
170
|
+
colorize("#{ICONS[:warning]} Hygiene", :yellow),
|
|
171
|
+
"1. Hygiene findings detected (#{finding_count}); rerun with --hygiene for details"
|
|
172
|
+
]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
output = ["", colorize("#{ICONS[:warning]} Hygiene (#{finding_count})", :yellow)]
|
|
176
|
+
issue_number = 1
|
|
177
|
+
checks.each do |check|
|
|
178
|
+
output << "#{issue_number}. #{check[:message]}"
|
|
179
|
+
Array(check[:details]).each { |detail| output << " - #{detail}" }
|
|
180
|
+
output << " Next: #{check[:next_action]}" if check[:next_action]
|
|
181
|
+
issue_number += 1
|
|
182
|
+
end
|
|
183
|
+
output
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def format_summary_line
|
|
187
|
+
health = @results[:health] || {}
|
|
188
|
+
hygiene = @results[:hygiene] || {}
|
|
189
|
+
info = @results[:info] || {}
|
|
190
|
+
parts = []
|
|
191
|
+
parts << colorize("#{health[:blocker_count]} blockers", health[:blocker_count].to_i.positive? ? :red : :green)
|
|
192
|
+
parts << colorize("#{health[:warning_count]} warnings", health[:warning_count].to_i.positive? ? :yellow : :green)
|
|
193
|
+
parts << colorize("#{info[:count]} info", :cyan)
|
|
194
|
+
parts << colorize("#{hygiene[:finding_count]} hygiene findings", hygiene[:finding_count].to_i.positive? ? :yellow : :green)
|
|
195
|
+
parts.join(", ")
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def final_status_line
|
|
199
|
+
if @results[:valid]
|
|
200
|
+
if @results.dig(:health, :warning_count).to_i.positive? || @results.dig(:hygiene, :finding_count).to_i.positive?
|
|
201
|
+
colorize("Setup check passed with warnings", :yellow)
|
|
202
|
+
else
|
|
203
|
+
colorize("Setup check passed", :green)
|
|
204
|
+
end
|
|
205
|
+
else
|
|
206
|
+
colorize("Setup check failed", :red)
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def provider_outcome_line(outcome)
|
|
211
|
+
elapsed = outcome[:elapsed_ms] ? " in #{outcome[:elapsed_ms]}ms" : ""
|
|
212
|
+
glyph = outcome[:status] == "pass" ? status_glyph(outcome[:status]) : colorize("â", :red)
|
|
213
|
+
suffix = if outcome[:status] == "pass"
|
|
214
|
+
elapsed
|
|
215
|
+
elsif outcome[:failure_type] == "timeout"
|
|
216
|
+
" timed out after #{outcome[:timeout_seconds]}s"
|
|
217
|
+
else
|
|
218
|
+
" failed"
|
|
219
|
+
end
|
|
220
|
+
"#{glyph} #{target_display(outcome)}#{suffix}"
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def target_display(target)
|
|
224
|
+
label = target[:label].to_s
|
|
225
|
+
selector = target[:selector].to_s
|
|
226
|
+
label = selector if label.empty?
|
|
227
|
+
selector.empty? || selector == label ? label : "#{label} (#{selector})"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def status_glyph(status)
|
|
231
|
+
glyph = STATUS_GLYPHS.fetch(status.to_s, STATUS_GLYPHS["warn"])
|
|
232
|
+
colorize(glyph, STATUS_COLORS.fetch(status.to_s, :yellow))
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def health_checks
|
|
236
|
+
@health_checks ||= checks.select { |check| check[:kind] == "health" }
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def hygiene_checks
|
|
240
|
+
@hygiene_checks ||= checks.select { |check| check[:kind] == "hygiene" }
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def info_checks
|
|
244
|
+
@info_checks ||= checks.select { |check| check[:kind] == "info" }
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def provider_outcomes
|
|
248
|
+
@provider_outcomes ||= Array(health_checks.find { |row| row[:id] == "probe-readiness" }&.fetch(:outcomes, []))
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def checks
|
|
252
|
+
@results[:checks] || []
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def format_duration(duration)
|
|
256
|
+
return "0ms" unless duration
|
|
257
|
+
|
|
258
|
+
duration < 1 ? "#{(duration * 1000).round}ms" : "#{duration.round(2)}s"
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def colorize(text, color)
|
|
262
|
+
return text unless @colors && COLORS[color]
|
|
263
|
+
|
|
264
|
+
"#{COLORS[color]}#{text}#{COLORS[:reset]}"
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require_relative "../models/config_templates"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Support
|
|
9
|
+
module Config
|
|
10
|
+
module Organisms
|
|
11
|
+
class ConfigSynchronizer
|
|
12
|
+
PROJECT_ROOT_DIR = "project-root"
|
|
13
|
+
GITIGNORE_ACE_LOCAL_ENTRY = ".ace-local/"
|
|
14
|
+
|
|
15
|
+
def initialize(force: false, dry_run: false, global: false, verbose: false)
|
|
16
|
+
@force = force
|
|
17
|
+
@dry_run = dry_run
|
|
18
|
+
@global = global
|
|
19
|
+
@verbose = verbose
|
|
20
|
+
@copied_files = []
|
|
21
|
+
@skipped_files = []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def sync_all
|
|
25
|
+
puts "Syncing all ace-* gem configurations..." if @verbose
|
|
26
|
+
|
|
27
|
+
Models::ConfigTemplates.all_gems.each do |gem_name|
|
|
28
|
+
sync_gem(gem_name)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
print_summary
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def sync_gem(gem_name)
|
|
35
|
+
gem_name = normalize_gem_name(gem_name)
|
|
36
|
+
|
|
37
|
+
unless Models::ConfigTemplates.gem_exists?(gem_name)
|
|
38
|
+
puts "Warning: No configuration found for #{gem_name}"
|
|
39
|
+
return
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
puts "\nSyncing #{gem_name}..." if @verbose
|
|
43
|
+
|
|
44
|
+
source_dir = Models::ConfigTemplates.example_dir_for(gem_name)
|
|
45
|
+
target_dir = target_directory
|
|
46
|
+
|
|
47
|
+
unless File.exist?(source_dir)
|
|
48
|
+
puts "Warning: No .ace-defaults directory found for #{gem_name}"
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
show_config_docs_if_needed(gem_name, target_dir)
|
|
53
|
+
copy_config_files(source_dir, target_dir)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def normalize_gem_name(name)
|
|
59
|
+
name.start_with?("ace-") ? name : "ace-#{name}"
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def target_directory
|
|
63
|
+
return File.expand_path("~/.ace") if @global
|
|
64
|
+
|
|
65
|
+
File.join(project_root, ".ace")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def show_config_docs_if_needed(gem_name, target_dir)
|
|
69
|
+
config_subdir = gem_name.sub("ace-", "")
|
|
70
|
+
existing_configs = Dir.glob("#{target_dir}/#{config_subdir}/**/*").reject { |f| File.directory?(f) }
|
|
71
|
+
|
|
72
|
+
return if existing_configs.any? || @dry_run
|
|
73
|
+
|
|
74
|
+
docs_file = Models::ConfigTemplates.docs_file_for(gem_name)
|
|
75
|
+
puts "\n#{File.read(docs_file)}\n" if docs_file && File.exist?(docs_file)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def copy_config_files(source_dir, target_dir)
|
|
79
|
+
glob_pattern = File.join(source_dir, "**", "*")
|
|
80
|
+
|
|
81
|
+
Dir.glob(glob_pattern, File::FNM_DOTMATCH).each do |source_file|
|
|
82
|
+
basename = File.basename(source_file)
|
|
83
|
+
next if basename == "." || basename == ".."
|
|
84
|
+
next if File.directory?(source_file)
|
|
85
|
+
|
|
86
|
+
relative_path = Pathname.new(source_file).relative_path_from(Pathname.new(source_dir))
|
|
87
|
+
target_file = target_file_for(relative_path, target_dir)
|
|
88
|
+
next unless target_file
|
|
89
|
+
|
|
90
|
+
copy_file(source_file, target_file)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def copy_file(source, target)
|
|
95
|
+
if File.basename(target) == ".gitignore"
|
|
96
|
+
merge_gitignore(source, target)
|
|
97
|
+
return
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if File.exist?(target) && !@force
|
|
101
|
+
@skipped_files << target
|
|
102
|
+
puts " Skipped: #{target} (already exists)" if @verbose
|
|
103
|
+
return
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
if @dry_run
|
|
107
|
+
puts " Would copy: #{source} -> #{target}"
|
|
108
|
+
else
|
|
109
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
110
|
+
FileUtils.cp(source, target)
|
|
111
|
+
@copied_files << target
|
|
112
|
+
puts " Copied: #{target}" if @verbose
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def target_file_for(relative_path, target_dir)
|
|
117
|
+
relative_str = relative_path.to_s
|
|
118
|
+
|
|
119
|
+
if relative_str.start_with?("#{PROJECT_ROOT_DIR}/")
|
|
120
|
+
return nil if @global
|
|
121
|
+
|
|
122
|
+
return File.join(project_root, relative_str.delete_prefix("#{PROJECT_ROOT_DIR}/"))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
File.join(target_dir, relative_str)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def merge_gitignore(source, target)
|
|
129
|
+
target_exists = File.exist?(target)
|
|
130
|
+
|
|
131
|
+
if target_exists
|
|
132
|
+
merge_gitignore_entry_if_missing(source, target)
|
|
133
|
+
return
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
if @dry_run
|
|
137
|
+
puts " Would copy: #{source} -> #{target}"
|
|
138
|
+
else
|
|
139
|
+
FileUtils.mkdir_p(File.dirname(target))
|
|
140
|
+
FileUtils.cp(source, target)
|
|
141
|
+
@copied_files << target
|
|
142
|
+
puts " Copied: #{target}" if @verbose
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def merge_gitignore_entry_if_missing(source, target)
|
|
147
|
+
existing_content = File.read(target)
|
|
148
|
+
return if gitignore_entry_present?(existing_content, GITIGNORE_ACE_LOCAL_ENTRY)
|
|
149
|
+
|
|
150
|
+
line = File.readlines(source, chomp: true).find do |entry|
|
|
151
|
+
entry == GITIGNORE_ACE_LOCAL_ENTRY
|
|
152
|
+
end
|
|
153
|
+
return unless line
|
|
154
|
+
|
|
155
|
+
if @dry_run
|
|
156
|
+
puts " Would append: #{line} -> #{target}"
|
|
157
|
+
return
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
File.open(target, "a") do |file|
|
|
161
|
+
file.write("\n") unless existing_content.end_with?("\n") || existing_content.empty?
|
|
162
|
+
file.puts line
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
@copied_files << target
|
|
166
|
+
puts " Appended: #{line} -> #{target}" if @verbose
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def gitignore_entry_present?(content, entry)
|
|
170
|
+
content.each_line.any? do |line|
|
|
171
|
+
normalized = line.strip
|
|
172
|
+
next false if normalized.empty? || normalized.start_with?("#")
|
|
173
|
+
|
|
174
|
+
normalized == entry
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def project_root
|
|
179
|
+
@project_root ||= Ace::Support::Config.find_project_root(start_path: Dir.pwd) || Dir.pwd
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def print_summary
|
|
183
|
+
return if @dry_run
|
|
184
|
+
|
|
185
|
+
puts "\nConfiguration sync complete:"
|
|
186
|
+
puts " Files copied: #{@copied_files.size}"
|
|
187
|
+
puts " Files skipped: #{@skipped_files.size}"
|
|
188
|
+
|
|
189
|
+
if @skipped_files.any? && !@force
|
|
190
|
+
puts "\nUse --force to overwrite existing files"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|