greenhat 0.3.2 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/lib/greenhat/accessors/disk.rb +42 -3
  3. data/lib/greenhat/accessors/gitlab.rb +30 -1
  4. data/lib/greenhat/accessors/memory.rb +1 -1
  5. data/lib/greenhat/archive.rb +11 -2
  6. data/lib/greenhat/cli.rb +37 -0
  7. data/lib/greenhat/host.rb +25 -37
  8. data/lib/greenhat/shell/args.rb +22 -9
  9. data/lib/greenhat/shell/faststats.rb +23 -3
  10. data/lib/greenhat/shell/field_helper.rb +1 -1
  11. data/lib/greenhat/shell/filter_help.rb +232 -9
  12. data/lib/greenhat/shell/log.rb +153 -9
  13. data/lib/greenhat/shell/markdown.rb +352 -0
  14. data/lib/greenhat/shell/old_search_helper.rb +54 -0
  15. data/lib/greenhat/shell/page.rb +1 -1
  16. data/lib/greenhat/shell/pipe.rb +31 -0
  17. data/lib/greenhat/shell/platform.rb +28 -0
  18. data/lib/greenhat/shell/report.rb +106 -25
  19. data/lib/greenhat/shell/shell_helper.rb +114 -106
  20. data/lib/greenhat/shell.rb +7 -0
  21. data/lib/greenhat/thing/file_types.rb +126 -7
  22. data/lib/greenhat/thing/formatters/json.rb +4 -0
  23. data/lib/greenhat/thing/formatters/kube_json.rb +36 -0
  24. data/lib/greenhat/thing/formatters/kube_nginx.rb +48 -0
  25. data/lib/greenhat/thing/formatters/kube_webservice.rb +51 -0
  26. data/lib/greenhat/thing/formatters/nginx.rb +6 -2
  27. data/lib/greenhat/thing/formatters/registry.rb +47 -0
  28. data/lib/greenhat/thing/formatters/time_space.rb +0 -16
  29. data/lib/greenhat/thing/helpers.rb +12 -0
  30. data/lib/greenhat/thing.rb +10 -0
  31. data/lib/greenhat/version.rb +1 -1
  32. data/lib/greenhat/views/api.slim +55 -0
  33. data/lib/greenhat/views/chart.slim +42 -0
  34. data/lib/greenhat/views/chart_template.slim +31 -0
  35. data/lib/greenhat/views/chartkick.js +21 -0
  36. data/lib/greenhat/views/css.slim +47 -0
  37. data/lib/greenhat/views/gitaly.slim +53 -0
  38. data/lib/greenhat/views/headers.slim +16 -0
  39. data/lib/greenhat/views/index-old.slim +51 -0
  40. data/lib/greenhat/views/index.slim +14 -14
  41. data/lib/greenhat/views/info.slim +17 -18
  42. data/lib/greenhat/views/production.slim +55 -0
  43. data/lib/greenhat/views/sidekiq.slim +55 -0
  44. data/lib/greenhat/views/time.slim +63 -0
  45. data/lib/greenhat/views/workhorse.slim +16 -0
  46. data/lib/greenhat/web/api.rb +94 -0
  47. data/lib/greenhat/web/chartkick_shim.rb +14 -0
  48. data/lib/greenhat/web/faststats.rb +44 -0
  49. data/lib/greenhat/web/gitaly.rb +65 -0
  50. data/lib/greenhat/web/helpers.rb +198 -0
  51. data/lib/greenhat/web/production.rb +104 -0
  52. data/lib/greenhat/web/sidekiq.rb +73 -0
  53. data/lib/greenhat/web/stats_helpers.rb +74 -0
  54. data/lib/greenhat/web/time.rb +36 -0
  55. data/lib/greenhat/web/workhorse.rb +43 -0
  56. data/lib/greenhat/web.rb +63 -19
  57. data/lib/greenhat.rb +1 -0
  58. metadata +73 -2
@@ -2,8 +2,8 @@ module GreenHat
2
2
  # CLI Helper
3
3
  module ShellHelper
4
4
  # Unify Filter / Filter Help
5
+ # rubocop:disable Metrics/MethodLength,Metrics/ModuleLength,Layout/LineLength
5
6
  module Filter
6
- # rubocop:disable Metrics/MethodLength
7
7
  def self.help
8
8
  puts "\u2500".pastel(:cyan) * 20
9
9
  puts 'Filter'.pastel(:yellow)
@@ -38,7 +38,7 @@ module GreenHat
38
38
  puts
39
39
 
40
40
  puts '--total'.pastel(:green)
41
- puts ' Print only total count of matching entries'
41
+ puts ' Show total count, duration, start/end time for matching entries'
42
42
  puts
43
43
 
44
44
  puts '--fields'.pastel(:green)
@@ -138,17 +138,39 @@ module GreenHat
138
138
  puts ' Ex: --truncate=200, --truncate=2048"'
139
139
  puts
140
140
 
141
- puts 'Field Searching'.pastel(:blue)
142
- puts ' --[key]=[value]'
143
- puts ' Search in key for value'
144
- puts ' Example: --path=mirror/pull'
145
- puts
146
-
147
141
  puts 'Search specific logs'.pastel(:blue)
148
142
  puts ' Any non dash parameters will be the log list to search from'
149
143
  puts " Ex: log filter --path=api sidekiq/current (hint: use `#{'ls'.pastel(:yellow)}` for log names"
150
144
  puts
151
145
 
146
+ puts 'General (Field) Searching'.pastel(:blue)
147
+ puts " There are different ways filter within specific keys/fields within log. The baseline is #{'--[key]=[value]'.pastel(:green)}"
148
+ puts ' This will, by default, select any entries with any partial match (case insensitive) of [value] in the entry [key]'
149
+ puts ' Example: --path=mirror/pull'
150
+ puts
151
+
152
+ puts " You can modify logic with params like: #{'case'.pastel(:green)}, #{'exact'.pastel(:green)}"
153
+ puts ' Ex Case turn on case sensitivity, exact will change from the inclusive to only exact matching.'
154
+ puts
155
+
156
+ puts " You can also use multiple #{'--[key]=[value]'.pastel(:green)} to narrow down, or combine it with #{'or'.pastel(:green)} to be more inclusive"
157
+ puts ' Ex nginx/gitlab_access.log --http_user_agent=gitlab-runner --path=/api/v4/jobs/request'
158
+ puts ' This will look through the access logs for anything that is a runner (user agent) and hitting the request endpoint'
159
+ puts
160
+
161
+ puts " You can also modify whether a field search excluded or a integer (and technically string) comparison by changing the operator: #{'(!= >= <=)'.pastel(:green)}"
162
+ puts ' Ex "nginx/gitlab_access.log --http_user_agent=gitlab-runner --http_user_agent!=13.12.0" will exclude any entries with 13.12.0/13.12 runners'
163
+ puts ' Ex "sidekiq/current --duration_s>=5 --class!=LdapSyncWorker" Duration over 5 that is not an ldap job'
164
+ puts
165
+
166
+ puts 'Pipe to Shell'.pastel(:blue)
167
+ puts ' This is very experimental! The idea is to take all the output from the search and allow a pipe into a traditional shell command.'
168
+ puts ' Due to the arg parsing its not a straight send to the terminal. You may need to escape quotes.'
169
+ puts ' Order is relevant, no arguments after the pipe'
170
+ puts
171
+ puts ' <files> <params> | grep path'
172
+ puts
173
+
152
174
  puts 'Example Queries'.pastel(:blue)
153
175
  puts " Also see #{'examples'.pastel(:bright_blue)} for even more examples"
154
176
  puts ' log filter --class=BuildFinishedWorker sidekiq/current --slice=time,message'
@@ -156,7 +178,208 @@ module GreenHat
156
178
 
157
179
  puts
158
180
  end
159
- # rubocop:enable Metrics/MethodLength
181
+
182
+ def self.help_index
183
+ {
184
+ title: [
185
+ "\u2500".pastel(:cyan) * 20,
186
+ 'Filter'.pastel(:yellow),
187
+ "\u2500".pastel(:cyan) * 20
188
+ ],
189
+ options: [
190
+ 'Options'.pastel(:blue)
191
+ ],
192
+
193
+ raw: [
194
+ '--raw'.pastel(:green),
195
+ ' Disable formatting and page/less'
196
+ ],
197
+
198
+ page: [
199
+ '--page'.pastel(:green),
200
+ ' Specifically enable or disable paging',
201
+ ' E.g. --page (default to true), --page=true, --page=false'
202
+ ],
203
+
204
+ round: ['--round'.pastel(:green),
205
+ ' Attempt to round all integers. Default: 2.',
206
+ ' E.g. --round, --round=3, --round=0'],
207
+
208
+ limit: [
209
+
210
+ '--limit'.pastel(:green),
211
+ ' Limit total output lines. Disabled by default. Default without value is based on screen height',
212
+ ' E.g. --limit, --limit=5'
213
+ ],
214
+
215
+ json: [
216
+ '--json'.pastel(:green),
217
+ ' Print output back into JSON'
218
+ ],
219
+
220
+ or: [
221
+ '--or'.pastel(:green),
222
+ ' Filters will use OR instead of AND (all match vs any match)'
223
+ ],
224
+
225
+ total: [
226
+ '--total'.pastel(:green),
227
+ ' Show total count, duration, start/end time for matching entries'
228
+ ],
229
+
230
+ fields: [
231
+ '--fields'.pastel(:green),
232
+ ' Print only Available fields for selected files'
233
+ ],
234
+
235
+ slice: [
236
+ '--slice'.pastel(:green),
237
+ ' Extract specific fields from entries (slice multiple with comma)',
238
+ ' Ex: --slice=path or --slice=path,params'
239
+
240
+ ],
241
+
242
+ except: [
243
+ '--except'.pastel(:green),
244
+ ' Exclude specific fields (except multiple with comma)',
245
+ ' Ex: --except=params --except=params,path'
246
+
247
+ ],
248
+
249
+ exists: [
250
+ '--exists'.pastel(:green),
251
+ ' Ensure field exists regardless of contents',
252
+ ' Ex: --exists=params --exists=params,path'
253
+
254
+ ],
255
+
256
+ stats: [
257
+ '--stats'.pastel(:green),
258
+ ' Order/Count occurrances by field. Combine with `truncate` for key names',
259
+ ' Ex: --stats=params --except=params,path'
260
+
261
+ ],
262
+
263
+ uniq: [
264
+ '--uniq'.pastel(:green),
265
+ ' Show unique values only',
266
+ ' Ex: --uniq=params --uniq=params,path'
267
+
268
+ ],
269
+
270
+ pluck: [
271
+ '--pluck'.pastel(:green),
272
+ ' Extract values from entries',
273
+ ' Ex: --pluck=params --pluck=params,path'
274
+
275
+ ],
276
+
277
+ archive: [
278
+ '--archive'.pastel(:green),
279
+ ' Limit to specific archvie name (partial matching /inclusive). Matching SOS tar.gz name',
280
+ ' Ex: --archive=dev-gitlab_20210622154626, --archive=202106,202107'
281
+
282
+ ],
283
+
284
+ sort: [
285
+ '--sort'.pastel(:green),
286
+ ' Sort by multiple fields',
287
+ ' Ex: --sort=duration_s,db_duration_s'
288
+
289
+ ],
290
+
291
+ reverse: [
292
+ '--reverse'.pastel(:green),
293
+ ' Reverse all results',
294
+ ' Ex: --reverse'
295
+
296
+ ],
297
+
298
+ combine: [
299
+ '--combine'.pastel(:green),
300
+ ' Omit archive identifier dividers. Useful with sort or time filters',
301
+ ' Ex: --combine'
302
+
303
+ ],
304
+
305
+ case: [
306
+ '--case'.pastel(:green),
307
+ ' Exact case match. Defaults to case insensitive',
308
+ ' Ex: --case; --name=Jon, --name=jane --case'
309
+
310
+ ],
311
+
312
+ exact: [
313
+ '--exact'.pastel(:green),
314
+ ' Exact parameter/value match. Defaults to partial match',
315
+ ' Ex: --field=CommonPartial --exact'
316
+
317
+ ],
318
+
319
+ start: [
320
+ '--start'.pastel(:green),
321
+ ' Show events after specified time. Filtered by the `time` field',
322
+ ' Use with `--end` for between selections',
323
+ ' Ex: log filter --start="2021-06-22 14:44 UTC" --end="2021-06-22 14:45 UTC"'
324
+
325
+ ],
326
+
327
+ end: [
328
+ '--end'.pastel(:green),
329
+ ' Show events before specified time. Filtered by the `time` field',
330
+ ' Use with `--start` for between selections',
331
+ ' Ex: log filter --end="2021-06-22"'
332
+ ],
333
+
334
+ time_zone: [
335
+ '--time_zone'.pastel(:green),
336
+ ' Manipulate the `time` field into a specific timezone',
337
+ ' Ex: log filter --time_zone=EDT'
338
+
339
+ ],
340
+
341
+ text: [
342
+ '--text'.pastel(:green),
343
+ ' Full entry text searching (slow)',
344
+ ' --text="anything here"'
345
+ ],
346
+
347
+ table_style: [
348
+ '--table_style'.pastel(:green),
349
+ ' Renderer used for formatted output. basic, ascii, or unicode(default)',
350
+ ' Ex: log filter --table_style=base'
351
+ ],
352
+
353
+ truncate: [
354
+ '--truncate'.pastel(:green),
355
+ ' Truncate field length. On by default (4 rows). Performance issues!',
356
+ ' Disable with --truncate=0'.pastel(:bright_red),
357
+ ' Ex: --truncate=200, --truncate=2048"'
358
+ ],
359
+
360
+ field: [
361
+ 'Field Searching'.pastel(:blue),
362
+ ' --[key]=[value]',
363
+ ' Search in key for value',
364
+ ' Example: --path=mirror/pull'
365
+ ],
366
+
367
+ ls: [
368
+ 'Search specific logs'.pastel(:blue),
369
+ ' Any non dash parameters will be the log list to search from',
370
+ " Ex: log filter --path=api sidekiq/current (hint: use `#{'ls'.pastel(:yellow)}` for log names"
371
+ ],
372
+
373
+ examples: [
374
+ 'Example Queries'.pastel(:blue),
375
+ " Also see #{'examples'.pastel(:bright_blue)} for even more examples",
376
+ ' log filter --class=BuildFinishedWorker sidekiq/current --slice=time,message',
377
+ ' log filter gitlab-rails/api_json.log --slice=ua --uniq=ua --ua=gitlab-runner'
378
+ ]
379
+
380
+ }
381
+ end
382
+ # rubocop:enable Metrics/MethodLength,Metrics/ModuleLength,Layout/LineLength
160
383
  end
161
384
  end
162
385
  end
@@ -2,6 +2,7 @@ module GreenHat
2
2
  # CLI Helper
3
3
  module Shell
4
4
  # Logs
5
+ # rubocop:disable Metrics/ModuleLength
5
6
  module Log
6
7
  def self.auto_complete(list, word)
7
8
  # Argument Parsing
@@ -47,6 +48,7 @@ module GreenHat
47
48
  puts ' filter'.pastel(:green)
48
49
  puts " Primary way for log searching within greenhat. See #{'filter_help'.pastel(:blue)}"
49
50
  puts ' Time, round, slice/except, and/or, stats, uniq, sort'
51
+ puts " #{'filter_help'.pastel(:blue)} supports filtering Ex: #{'filter_help stats'.pastel(:blue)}"
50
52
  puts
51
53
 
52
54
  puts ' show'.pastel(:green)
@@ -57,13 +59,33 @@ module GreenHat
57
59
  puts " General full text by file searching. See #{'search_help'.pastel(:blue)}"
58
60
  puts
59
61
 
62
+ puts ' save'.pastel(:green)
63
+ puts ' Save the last query result into a new searchable object'
64
+ puts
65
+
66
+ puts ' write'.pastel(:green)
67
+ puts ' Write the last query result into a local file'
68
+ puts
69
+
70
+ puts ' visualize'.pastel(:green)
71
+ puts ' Load web services and formulate last query for the UI'
72
+ puts
73
+
60
74
  puts ShellHelper::List.help
61
75
 
62
76
  puts "See #{'examples'.pastel(:bright_blue)} for query examples"
63
77
  end
64
78
 
65
- def self.filter_help
66
- ShellHelper::Filter.help
79
+ def self.filter_help(args = {})
80
+ if args.empty?
81
+ ShellHelper::Filter.help
82
+ else
83
+ list = ShellHelper::Filter.help_index.select do |k, _v|
84
+ k.to_s.include? args.first
85
+ end
86
+
87
+ puts list.values.map { |x| x.join("\n") }.join("\n\n")
88
+ end
67
89
  end
68
90
 
69
91
  def self.ls(args = [])
@@ -80,6 +102,95 @@ module GreenHat
80
102
  ShellHelper.show files.map(&:data).flatten
81
103
  end
82
104
 
105
+ def self.save(raw = [])
106
+ if ShellHelper::Log.last.nil?
107
+ puts 'No previous query found'.pastel(:red)
108
+ puts 'Run a query first then save to store as a new log'
109
+ puts
110
+ puts "Try #{'nginx/gitlab_access.log --status!=200'.pastel(:green)} then #{'save'.pastel(:green)}"
111
+ true
112
+ end
113
+
114
+ name = if raw.empty?
115
+ Cli.prompt.ask('Log/name to save the results to? '.pastel(:yellow))
116
+ else
117
+ raw.first
118
+ end
119
+
120
+ if name.blank?
121
+ puts 'Name required'.pastel(:red)
122
+ return true
123
+ end
124
+
125
+ results = ShellHelper.filter_internal ShellHelper::Log.last
126
+
127
+ # Don't save empty results
128
+ if results.empty?
129
+ puts 'No results'.pastel(:red)
130
+ ShellHelper::Log.no_files_warning(files) if ShellHelper.find_things(files, flags).count.zero?
131
+ return false
132
+ end
133
+
134
+ Thing.new.query_save(results, name)
135
+ puts "#{name.pastel(:green)} Saved!"
136
+ end
137
+
138
+ def self.write(raw = [])
139
+ if ShellHelper::Log.last.nil?
140
+ puts 'No previous query found'.pastel(:red)
141
+ puts 'Run a query first then write'
142
+ puts
143
+ puts "Try #{'nginx/gitlab_access.log --status!=200'.pastel(:green)} then #{'save'.pastel(:green)}"
144
+ true
145
+ end
146
+
147
+ name = if raw.empty?
148
+ Cli.prompt.ask('Log/name to save the results to? '.pastel(:yellow))
149
+ else
150
+ raw.first
151
+ end
152
+
153
+ if name.blank?
154
+ puts 'Name required'.pastel(:red)
155
+ return true
156
+ end
157
+
158
+ results = ShellHelper.filter_internal ShellHelper::Log.last
159
+
160
+ # Don't save empty results
161
+ if results.empty?
162
+ puts 'No results'.pastel(:red)
163
+ ShellHelper::Log.no_files_warning(files) if ShellHelper.find_things(files, flags).count.zero?
164
+ return false
165
+ end
166
+
167
+ all = results.map { |row| Oj.dump(row) }
168
+ File.write(name, all.join("\n"))
169
+ puts "#{name.pastel(:green)} File Written!"
170
+ end
171
+
172
+ def self.visualize
173
+ if ShellHelper::Log.last.nil?
174
+ puts 'No previous query found'.pastel(:red)
175
+ puts 'Run a query first then visualize to load it in the chart web page'
176
+ puts
177
+ puts "Try #{'nginx/gitlab_access.log --status!=200'.pastel(:green)} then #{'visualize'.pastel(:green)}"
178
+ return true
179
+ end
180
+
181
+ # Load Required Files
182
+ require 'greenhat/web'
183
+
184
+ unless GreenHat::Web.alive?
185
+ GreenHat::Web.start
186
+ sleep 0.2
187
+ end
188
+
189
+ url = "http://localhost:4567/chart/time?query=#{CGI.escape(ShellHelper::Log.last)}"
190
+
191
+ GreenHat::Platform.open url
192
+ end
193
+
83
194
  # ========================================================================
84
195
  # Filter (See Filter Help)
85
196
  # ========================================================================
@@ -94,6 +205,8 @@ module GreenHat
94
205
  return true
95
206
  end
96
207
 
208
+ ShellHelper::Log.last = raw
209
+
97
210
  # Argument Parsing
98
211
  files, flags, args = Args.parse(raw)
99
212
 
@@ -104,7 +217,7 @@ module GreenHat
104
217
 
105
218
  # Skip and Print Total if set
106
219
  if flags[:total]
107
- ShellHelper.total_count(results)
220
+ ShellHelper.total_count(results, flags)
108
221
  return true
109
222
  end
110
223
 
@@ -117,13 +230,13 @@ module GreenHat
117
230
  # Check Search Results
118
231
  if results.instance_of?(Hash) && results.values.flatten.empty?
119
232
  puts 'No results'.pastel(:red)
233
+ ShellHelper::Log.no_files_warning(files) if ShellHelper.find_things(files, flags).count.zero?
234
+ elsif flags[:pipe]
235
+ Pipe.show(results, flags[:pipe])
120
236
  else
121
237
  # This causes the key 'colorized' output to also be included
122
238
  ShellHelper.show(results.to_a.compact.flatten, flags)
123
239
  end
124
-
125
- # log filter --path='cloud/gitlab-automation' --path='/pull' --all
126
- # log filter --project=thingy --other_filter=asdf *
127
240
  rescue StandardError => e
128
241
  LogBot.fatal('Filter', message: e.message)
129
242
  ap e.backtrace
@@ -152,9 +265,25 @@ module GreenHat
152
265
  puts 'Count/% occurences for both user and remote ip fields'.pastel(:bright_green)
153
266
  puts 'gitlab-rails/api_json.log --stats=meta.user,meta.remote_ip --exists=meta.user'
154
267
  puts
155
- end
156
268
 
269
+ puts 'Sidekiq jobs that took over 5 seconds excluding LdapSyncWorker jobs'.pastel(:bright_green)
270
+ puts 'sidekiq/current --duration_s>=5 --class!=LdapSyncWorker'
271
+ puts
272
+
273
+ puts 'Search access logs for runner requests, exclude specific runner version'.pastel(:bright_green)
274
+ puts 'nginx/gitlab_access.log --http_user_agent=gitlab-runner --http_user_agent!=13.12.0'
275
+ puts
276
+
277
+ puts 'Get a list of unique Gitaly error messages for a specific project'.pastel(:bright_green)
278
+ puts 'filter --level=error --grpc.request.glProjectPath=path/to/project gitaly/current --slice=error --uniq=error'
279
+ puts
280
+
281
+ puts 'Show workhorse duration/URI. Filter by duration bounds'.pastel(:bright_green)
282
+ puts 'gitlab-workhorse/current --duration_ms>=30000 --duration_ms<=45000 --slice=duration_ms,uri'
283
+ puts
284
+ end
157
285
  # rubocop:enable Layout/LineLength
286
+
158
287
  # ========================================================================
159
288
  # Search (Full Text / String Search)
160
289
  # ========================================================================
@@ -169,13 +298,15 @@ module GreenHat
169
298
 
170
299
  # Skip and Print Total if set
171
300
  if flags[:total]
172
- ShellHelper.total_count(results)
301
+ ShellHelper.total_count(results, flags)
173
302
  return true
174
303
  end
175
304
 
176
305
  # Check Search Results
177
306
  if results.values.flatten.empty?
178
307
  puts 'No results'.pastel(:red)
308
+ ShellHelper::Log.no_files_warning(files) if ShellHelper.find_things(files, flags).count.zero?
309
+
179
310
  else
180
311
  # This causes the key 'colorized' output to also be included
181
312
  ShellHelper.show(results.to_a.compact.flatten, flags)
@@ -240,7 +371,7 @@ module GreenHat
240
371
  puts 'log search --text=BuildHooksWorker --text!=start --slice=enqueued_at sidekiq/current'
241
372
  puts
242
373
  end
243
- # rubocop:enable Metrics/MethodLength
374
+ # rubocop:enable Metrics/MethodLength,Metrics/ModuleLength
244
375
 
245
376
  # ------------------------------------------------------------------------
246
377
  end
@@ -251,9 +382,22 @@ module GreenHat
251
382
  module ShellHelper
252
383
  # Log Helpers
253
384
  module Log
385
+ def self.last=(value)
386
+ @last = value.join(' ')
387
+ end
388
+
389
+ def self.last
390
+ @last
391
+ end
392
+
254
393
  def self.list
255
394
  Thing.all.select(&:log)
256
395
  end
396
+
397
+ def self.no_files_warning(files)
398
+ puts "No matching files found for pattern #{files.to_s.pastel(:yellow)}"
399
+ puts "See #{'ls'.pastel(:blue)} for available files"
400
+ end
257
401
  end
258
402
  # --------
259
403
  end