belt 0.2.12 → 0.2.13

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10e92ad2ab3fe79f48f07cca6495ce8b9ccdad09755c7a6517e651deadcc118d
4
- data.tar.gz: 6cd8a8ee7ad1023fb949183122c3aca465c216cf91d175e8f9395e8f413ad880
3
+ metadata.gz: 43bc6e6cb774005faa3bab6b5133403255636327e3dd017ad4acad8fd9a2c884
4
+ data.tar.gz: fe11f89e1efa821750dea2953c70bbc21fe24bea394d418c2b870a2804adf331
5
5
  SHA512:
6
- metadata.gz: cb7d5dacc2b90e0ae7c4dd9853d76ea42ef8d6e324d30edd91c5fbe6d934affafd2c3bee6fb752960d9c8462a59663244ae448797ab7e57de7ad05c50afa627f
7
- data.tar.gz: fd7f4ff9af68c36b34c02c2f4af2c42649f0e9494ac192eaaefeae3111e0da81ced4091b88510b82ac183fefaadfd0677e4d34f270e16a7dcf389b54259e8c52
6
+ metadata.gz: d5c4900624a6435b178455b5aa7e6f34c08d7c2e75e2d1ec3c1b69e717a5964a390ba59a4ff8ccad1be797d29273bddc4354c235f83070d00bb7517d60bcb38e
7
+ data.tar.gz: a47bc3063743a278e4241faf65c510ede23ec922c167469f2654627f3079bb558893194f717979002354e07153e41cee257785e8b0f7dea187fc391fb351e5f3
data/CHANGELOG.md CHANGED
@@ -31,7 +31,17 @@ Previously, contracts were only accessible as a side-effect of `belt routes -f j
31
31
  - Module template updated: `source` points to `config/routes.rb`
32
32
  - All scaffold/generator help text and templates reference new filenames
33
33
 
34
- ## Unreleased
34
+ ## 0.2.13
35
+
36
+ ### Template cleanup
37
+
38
+ - **Model template**: single-line `attr_accessor` instead of one per attribute; removed redundant `to_h` (ActiveItem::Base already provides it)
39
+ - **Controller template**: uses implicit response pattern (instance variable assigns) instead of explicit `success_response` calls; `response_status :created` for create actions, `head :no_content` for destroy
40
+ - **Gemfile template**: removed `activeitem` and `lambda_loadout` (already belt gem dependencies)
41
+ - **gitignore template**: excludes `lambda/lib/routes/` (generated artifact from `belt routes`)
42
+ - **Removed `require 'activeitem'`** from api.rb, environment.rb, and application_record.rb templates (belt requires it transitively)
43
+ - **Removed `ActiveItem.configure` boilerplate** from api.rb and environment.rb templates (activeitem 0.0.13+ defaults `table_prefix` and `environment` from ENV vars)
44
+ - Bumped activeitem dependency to `>= 0.0.13`
35
45
 
36
46
  ## 0.2.11
37
47
 
@@ -59,6 +59,7 @@ module Belt
59
59
 
60
60
  install_dependencies(dest_dir)
61
61
  setup_frontend_infra_for_existing_environments
62
+ generate_views_for_existing_resources
62
63
 
63
64
  return if @quiet || !@announce
64
65
 
@@ -104,6 +105,28 @@ module Belt
104
105
  puts " create #{frontend_tf}" unless @quiet
105
106
  end
106
107
 
108
+ def generate_views_for_existing_resources
109
+ routes_file = find_routes_file_path
110
+ return unless routes_file && File.exist?(routes_file)
111
+
112
+ resources = extract_resources_from_routes(routes_file)
113
+ return if resources.empty?
114
+
115
+ puts "\n Detected existing resources: #{resources.join(', ')}" unless @quiet
116
+ puts ' Generating views...' unless @quiet
117
+
118
+ require_relative 'views_command'
119
+ resources.each do |resource_name|
120
+ fields = ViewsCommand.read_schema_fields(resource_name)
121
+ ViewsCommand.new(resource_name, fields, force: true).generate
122
+ end
123
+ end
124
+
125
+ def extract_resources_from_routes(routes_file)
126
+ content = File.read(routes_file)
127
+ content.scan(/resources\s+:(\w+)/).flatten.uniq
128
+ end
129
+
107
130
  def copy_template(src_dir, dest_dir)
108
131
  Dir.glob("#{src_dir}/**/*", File::FNM_DOTMATCH).each do |src|
109
132
  next if File.directory?(src)
@@ -478,14 +478,16 @@ module Belt
478
478
  puts " update #{schema_file}"
479
479
  end
480
480
 
481
- # Map generator field types to OpenAPI schema DSL types
481
+ # Map generator field types to schema DSL types.
482
+ # Preserves :text, :date, :datetime so views can render appropriate form inputs.
482
483
  def schema_type_for(field_type)
483
484
  case field_type.to_s
484
- when 'text' then 'string'
485
+ when 'text' then 'text'
485
486
  when 'integer' then 'integer'
486
487
  when 'float' then 'number'
487
488
  when 'boolean' then 'boolean'
488
- when 'date', 'datetime' then 'string'
489
+ when 'date' then 'date'
490
+ when 'datetime' then 'datetime'
489
491
  else 'string'
490
492
  end
491
493
  end
@@ -505,7 +507,7 @@ module Belt
505
507
  return unless Dir.exist?('frontend/src')
506
508
  return if @skip_views
507
509
 
508
- Belt::CLI::ViewsCommand.new(@name, @fields).generate
510
+ Belt::CLI::ViewsCommand.new(@name, @fields, force: @force).generate
509
511
  end
510
512
  end
511
513
  end
@@ -0,0 +1,634 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'open3'
5
+
6
+ module Belt
7
+ module CLI
8
+ class LogsCommand
9
+ include AppDetection
10
+
11
+ COLORS = {
12
+ red: "\e[0;31m",
13
+ green: "\e[0;32m",
14
+ yellow: "\e[0;33m",
15
+ blue: "\e[0;34m",
16
+ magenta: "\e[0;35m",
17
+ cyan: "\e[0;36m",
18
+ gray: "\e[0;90m",
19
+ bold: "\e[1m",
20
+ dim: "\e[2m",
21
+ reset: "\e[0m"
22
+ }.freeze
23
+
24
+ def self.run(args)
25
+ if args.include?('--help') || args.include?('-h')
26
+ puts usage
27
+ exit 0
28
+ end
29
+
30
+ new(args).run
31
+ end
32
+
33
+ def self.usage
34
+ <<~USAGE
35
+ Usage: belt logs [lambda] [options]
36
+
37
+ View Lambda function logs. Without arguments, tails all Lambdas for the current environment.
38
+
39
+ Arguments:
40
+ lambda Lambda function short name (e.g., api, worker). Optional — shows all if omitted.
41
+
42
+ Options:
43
+ -e, --env ENV Environment (default: BELT_ENV or first detected)
44
+ -f, --follow Follow logs in real-time
45
+ -s, --since PERIOD Time range (default: 5m). Examples: 5m, 30m, 1h, 2h
46
+ -l, --level LEVEL Minimum log level: DEBUG, INFO, WARN, ERROR (default: INFO)
47
+ --error Show only the most recent error and exit
48
+ --raw Show raw JSON logs without formatting
49
+ --no-color Disable colorized output
50
+ -h, --help Show this help
51
+
52
+ Examples:
53
+ belt logs # Last 5m of all lambdas (current env)
54
+ belt logs api # Last 5m of api lambda
55
+ belt logs api -f # Follow api lambda logs
56
+ belt logs -e prod # Last 5m of all lambdas in prod
57
+ belt logs api -s 30m # Last 30 minutes
58
+ belt logs --error # Show most recent error across all lambdas
59
+ belt logs api --error # Show most recent error for api lambda
60
+ USAGE
61
+ end
62
+
63
+ def initialize(args)
64
+ @lambda_name = nil
65
+ @env = nil
66
+ @follow = false
67
+ @since = '5m'
68
+ @level = 'INFO'
69
+ @error_mode = false
70
+ @raw = false
71
+ @color = $stdout.tty?
72
+ parse_args(args)
73
+ end
74
+
75
+ def run
76
+ @env ||= detect_environment
77
+ abort 'Error: Cannot determine environment. Pass -e ENV or set BELT_ENV.' unless @env
78
+
79
+ @app_name = detect_app_name
80
+ abort 'Error: Cannot determine app name.' unless @app_name
81
+
82
+ if @lambda_name
83
+ tail_single(@lambda_name)
84
+ else
85
+ tail_all
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def parse_args(args)
92
+ i = 0
93
+ while i < args.length
94
+ case args[i]
95
+ when '-e', '--env'
96
+ @env = args[i + 1]
97
+ i += 2
98
+ when '-f', '--follow'
99
+ @follow = true
100
+ i += 1
101
+ when '-s', '--since'
102
+ @since = args[i + 1]
103
+ i += 2
104
+ when '-l', '--level'
105
+ @level = args[i + 1]&.upcase
106
+ i += 2
107
+ when '--error'
108
+ @error_mode = true
109
+ i += 1
110
+ when '--raw'
111
+ @raw = true
112
+ i += 1
113
+ when '--no-color'
114
+ @color = false
115
+ i += 1
116
+ else
117
+ @lambda_name = args[i] unless args[i].start_with?('-')
118
+ i += 1
119
+ end
120
+ end
121
+ end
122
+
123
+ def detect_environment
124
+ ENV.fetch('BELT_ENV', nil) || detect_environments.first
125
+ end
126
+
127
+ def tail_single(lambda_name)
128
+ log_group = log_group_for(lambda_name)
129
+
130
+ unless log_group_exists?(log_group)
131
+ abort "#{c(:red)}✗ No log group found: #{log_group}#{c(:reset)}\n " \
132
+ 'The Lambda may not have been deployed yet.'
133
+ end
134
+
135
+ if @error_mode
136
+ find_recent_error(log_group, lambda_name)
137
+ elsif @follow
138
+ follow_logs(log_group, lambda_name)
139
+ else
140
+ fetch_historical(log_group, lambda_name)
141
+ end
142
+ end
143
+
144
+ def tail_all
145
+ lambdas = discover_lambda_names
146
+ if lambdas.empty?
147
+ abort "#{c(:red)}✗ No Lambda functions found for #{@app_name}-#{@env}#{c(:reset)}\n " \
148
+ "Deploy with `belt deploy #{@env}` first, or specify a lambda name: `belt logs api`"
149
+ end
150
+
151
+ if @error_mode
152
+ find_errors_across(lambdas)
153
+ elsif @follow
154
+ follow_all(lambdas)
155
+ else
156
+ fetch_all_historical(lambdas)
157
+ end
158
+ end
159
+
160
+ def discover_lambda_names
161
+ names = lambda_names_from_terraform
162
+ return names if names.any?
163
+
164
+ lambda_names_from_log_groups
165
+ end
166
+
167
+ def lambda_names_from_terraform
168
+ infra_dir = find_infra_dir
169
+ env_dir = infra_dir ? File.join(infra_dir, @env) : nil
170
+ return [] unless env_dir && Dir.exist?(File.join(env_dir, '.terraform'))
171
+
172
+ Dir.chdir(env_dir) do
173
+ output, status = Open3.capture2('terraform', 'output', '-json')
174
+ return [] unless status.success?
175
+
176
+ data = begin
177
+ JSON.parse(output)
178
+ rescue JSON::ParserError
179
+ {}
180
+ end
181
+
182
+ if data['lambda_functions']
183
+ funcs = data['lambda_functions']['value']
184
+ if funcs.is_a?(Hash)
185
+ return funcs.keys
186
+ elsif funcs.is_a?(Array)
187
+ return funcs.map { |f| f.is_a?(String) ? f.split('-').last : nil }.compact
188
+ end
189
+ end
190
+
191
+ data.keys.grep(/_function_name$/).map { |k| data[k]['value']&.split('-')&.last }.compact
192
+ end
193
+ rescue StandardError
194
+ []
195
+ end
196
+
197
+ def lambda_names_from_log_groups
198
+ prefix = "/aws/lambda/#{@app_name}-#{@env}-"
199
+ output, status = Open3.capture2(
200
+ 'aws', 'logs', 'describe-log-groups',
201
+ '--log-group-name-prefix', prefix,
202
+ '--query', 'logGroups[].logGroupName',
203
+ '--output', 'json'
204
+ )
205
+ return [] unless status.success?
206
+
207
+ groups = begin
208
+ JSON.parse(output)
209
+ rescue JSON::ParserError
210
+ []
211
+ end
212
+ groups.map { |g| g.sub(prefix, '') }
213
+ end
214
+
215
+ def log_group_for(lambda_name)
216
+ "/aws/lambda/#{@app_name}-#{@env}-#{lambda_name}"
217
+ end
218
+
219
+ def log_group_exists?(log_group)
220
+ _, status = Open3.capture2(
221
+ 'aws', 'logs', 'describe-log-groups',
222
+ '--log-group-name-prefix', log_group,
223
+ '--query', "logGroups[?logGroupName=='#{log_group}'].logGroupName",
224
+ '--output', 'text'
225
+ )
226
+ status.success?
227
+ end
228
+
229
+ def follow_logs(log_group, lambda_name)
230
+ print_header(lambda_name)
231
+ puts "#{c(:yellow)}Following logs (Ctrl+C to stop)...#{c(:reset)}\n\n"
232
+
233
+ cmd = ['aws', 'logs', 'tail', log_group, '--follow', '--format', 'short']
234
+ IO.popen(cmd, err: %i[child out]) do |io|
235
+ io.each_line { |line| process_tail_line(line) }
236
+ end
237
+ rescue Interrupt
238
+ puts "\n#{c(:dim)}Stopped.#{c(:reset)}"
239
+ end
240
+
241
+ def follow_all(lambdas)
242
+ puts "#{c(:cyan)}═══════════════════════════════════════════════════════════════#{c(:reset)}"
243
+ puts "#{c(:bold)}Lambda Logs: #{c(:magenta)}#{@app_name}-#{@env}#{c(:reset)} (#{lambdas.join(', ')})"
244
+ puts "#{c(:cyan)}═══════════════════════════════════════════════════════════════#{c(:reset)}"
245
+ puts "#{c(:yellow)}Following logs (Ctrl+C to stop)...#{c(:reset)}\n\n"
246
+
247
+ threads = lambdas.map do |name|
248
+ log_group = log_group_for(name)
249
+ Thread.new do
250
+ cmd = ['aws', 'logs', 'tail', log_group, '--follow', '--format', 'short']
251
+ IO.popen(cmd, err: %i[child out]) do |io|
252
+ io.each_line { |line| process_tail_line(line, prefix: name) }
253
+ end
254
+ rescue StandardError
255
+ nil
256
+ end
257
+ end
258
+
259
+ threads.each(&:join)
260
+ rescue Interrupt
261
+ puts "\n#{c(:dim)}Stopped.#{c(:reset)}"
262
+ end
263
+
264
+ def fetch_historical(log_group, lambda_name)
265
+ print_header(lambda_name)
266
+ puts "#{c(:dim)}Fetching last #{@since} of logs...#{c(:reset)}\n\n"
267
+
268
+ events = fetch_log_events(log_group)
269
+ if events.empty?
270
+ puts "#{c(:yellow)}No logs found in the last #{@since}#{c(:reset)}"
271
+ return
272
+ end
273
+
274
+ puts "#{c(:dim)}Found #{events.length} log events#{c(:reset)}\n\n"
275
+ events.each { |event| process_event(event) }
276
+
277
+ puts "\n#{c(:cyan)}═══════════════════════════════════════════════════════════════#{c(:reset)}"
278
+ puts "#{c(:dim)}Tip: Use -f to follow logs in real-time#{c(:reset)}"
279
+ end
280
+
281
+ def fetch_all_historical(lambdas)
282
+ puts "#{c(:cyan)}═══════════════════════════════════════════════════════════════#{c(:reset)}"
283
+ puts "#{c(:bold)}Lambda Logs: #{c(:magenta)}#{@app_name}-#{@env}#{c(:reset)} (#{lambdas.join(', ')})"
284
+ puts "#{c(:cyan)}═══════════════════════════════════════════════════════════════#{c(:reset)}"
285
+ puts "#{c(:dim)}Fetching last #{@since} of logs...#{c(:reset)}\n\n"
286
+
287
+ all_events = []
288
+ lambdas.each do |name|
289
+ log_group = log_group_for(name)
290
+ events = fetch_log_events(log_group)
291
+ events.each { |e| e['_lambda'] = name }
292
+ all_events.concat(events)
293
+ end
294
+
295
+ all_events.sort_by! { |e| e['timestamp'] || 0 }
296
+
297
+ if all_events.empty?
298
+ puts "#{c(:yellow)}No logs found in the last #{@since}#{c(:reset)}"
299
+ return
300
+ end
301
+
302
+ puts "#{c(:dim)}Found #{all_events.length} log events#{c(:reset)}\n\n"
303
+ all_events.each { |event| process_event(event, prefix: event['_lambda']) }
304
+
305
+ puts "\n#{c(:cyan)}═══════════════════════════════════════════════════════════════#{c(:reset)}"
306
+ puts "#{c(:dim)}Tip: Use -f to follow logs in real-time#{c(:reset)}"
307
+ end
308
+
309
+ def find_recent_error(log_group, lambda_name)
310
+ events = fetch_log_events(log_group, since: '30m', limit: 500)
311
+ error = find_error_in_events(events)
312
+
313
+ if error
314
+ puts "#{c(:bold)}Most recent error in #{c(:magenta)}#{lambda_name}#{c(:reset)}:\n\n"
315
+ format_error_event(error)
316
+ else
317
+ puts "#{c(:green)}✓ No errors found in the last 30 minutes for #{lambda_name}#{c(:reset)}"
318
+ end
319
+ end
320
+
321
+ def find_errors_across(lambdas)
322
+ latest_error = nil
323
+ latest_lambda = nil
324
+
325
+ lambdas.each do |name|
326
+ log_group = log_group_for(name)
327
+ events = fetch_log_events(log_group, since: '30m', limit: 500)
328
+ error = find_error_in_events(events)
329
+ next unless error
330
+
331
+ if latest_error.nil? || (error['timestamp'] || 0) > (latest_error['timestamp'] || 0)
332
+ latest_error = error
333
+ latest_lambda = name
334
+ end
335
+ end
336
+
337
+ if latest_error
338
+ puts "#{c(:bold)}Most recent error in #{c(:magenta)}#{latest_lambda}#{c(:reset)}:\n\n"
339
+ format_error_event(latest_error)
340
+ else
341
+ puts "#{c(:green)}✓ No errors found in the last 30 minutes#{c(:reset)}"
342
+ end
343
+ end
344
+
345
+ def find_error_in_events(events)
346
+ events.reverse_each do |event|
347
+ msg = event['message'] || ''
348
+ json = parse_json(msg)
349
+ next unless json
350
+
351
+ return event if json['errorMessage'] && json['errorType'] && json['stackTrace']
352
+ return event if json['level'] == 'ERROR'
353
+ return event if json['status_code'].to_i >= 500
354
+ end
355
+ nil
356
+ end
357
+
358
+ def format_error_event(event)
359
+ msg = event['message'] || ''
360
+ json = parse_json(msg)
361
+ return puts(msg) unless json
362
+
363
+ timestamp = format_timestamp(event['timestamp'])
364
+
365
+ if json['errorMessage'] && json['errorType']
366
+ format_init_error(json, timestamp)
367
+ else
368
+ format_structured_log(json, timestamp)
369
+ end
370
+ end
371
+
372
+ def format_init_error(json, timestamp)
373
+ puts "#{c(:gray)}#{timestamp}#{c(:reset)} #{c(:red)}#{c(:bold)}INIT ERROR#{c(:reset)}"
374
+ puts " #{c(:dim)}Type:#{c(:reset)} #{c(:red)}#{json['errorType']}#{c(:reset)}"
375
+ puts " #{c(:dim)}Message:#{c(:reset)} #{json['errorMessage']}"
376
+ return unless json['stackTrace'].is_a?(Array)
377
+
378
+ puts " #{c(:dim)}Stack:#{c(:reset)}"
379
+ json['stackTrace'].first(15).each do |line|
380
+ if line.match?(%r{(controllers|models|lib|helpers)/})
381
+ puts " #{c(:yellow)}→#{c(:reset)} #{line}"
382
+ elsif line.include?('/var/task/')
383
+ puts " #{c(:cyan)}→#{c(:reset)} #{line}"
384
+ else
385
+ puts " #{c(:gray)} #{line}#{c(:reset)}"
386
+ end
387
+ end
388
+ end
389
+
390
+ def fetch_log_events(log_group, since: @since, limit: 1000)
391
+ duration_ms = parse_since(since)
392
+ start_time = (Time.now.to_i * 1000) - duration_ms
393
+
394
+ output, status = Open3.capture2(
395
+ 'aws', 'logs', 'filter-log-events',
396
+ '--log-group-name', log_group,
397
+ '--start-time', start_time.to_s,
398
+ '--limit', limit.to_s,
399
+ '--output', 'json'
400
+ )
401
+ return [] unless status.success?
402
+
403
+ data = begin
404
+ JSON.parse(output)
405
+ rescue JSON::ParserError
406
+ {}
407
+ end
408
+ data['events'] || []
409
+ end
410
+
411
+ def process_tail_line(line, prefix: nil)
412
+ return if line.match?(/\b(START|END|REPORT|INIT_START|INIT_REPORT)\s/)
413
+ return if line.include?('[LambdaLoadout') || line.include?('"_aws":')
414
+
415
+ if line.include?('Critical exception from handler')
416
+ prefix_str = prefix ? "#{c(:blue)}[#{prefix}]#{c(:reset)} " : ''
417
+ puts "#{prefix_str}#{c(:red)}#{c(:bold)}CRITICAL EXCEPTION#{c(:reset)}"
418
+ return
419
+ end
420
+
421
+ json_part = line.sub(/\A[\dT:.+\-Z ]+\s*/, '').strip
422
+ json = parse_json(json_part)
423
+
424
+ if json
425
+ if json['errorMessage'] && json['errorType'] && json['stackTrace']
426
+ format_aws_error(json, prefix: prefix)
427
+ elsif @raw
428
+ puts json_part
429
+ else
430
+ timestamp = extract_time_from_line(line)
431
+ format_structured_log(json, timestamp, prefix: prefix)
432
+ end
433
+ elsif line.strip.length.positive?
434
+ prefix_str = prefix ? "#{c(:blue)}[#{prefix}]#{c(:reset)} " : ''
435
+ puts "#{prefix_str}#{c(:gray)}#{line.strip}#{c(:reset)}"
436
+ end
437
+ end
438
+
439
+ def process_event(event, prefix: nil)
440
+ msg = event['message'] || ''
441
+
442
+ return if msg.match?(/\A(START|END|REPORT|INIT_START|INIT_REPORT)\s/)
443
+ return if msg.include?('[LambdaLoadout') || msg.include?('"_aws":')
444
+
445
+ if msg.include?('Critical exception from handler')
446
+ prefix_str = prefix ? "#{c(:blue)}[#{prefix}]#{c(:reset)} " : ''
447
+ puts "#{prefix_str}#{c(:red)}#{c(:bold)}CRITICAL EXCEPTION#{c(:reset)}"
448
+ return
449
+ end
450
+
451
+ json = parse_json(msg)
452
+ timestamp = format_timestamp(event['timestamp'])
453
+
454
+ if json
455
+ if json['errorMessage'] && json['errorType'] && json['stackTrace']
456
+ format_aws_error(json, prefix: prefix)
457
+ elsif @raw
458
+ puts JSON.pretty_generate(json)
459
+ else
460
+ format_structured_log(json, timestamp, prefix: prefix)
461
+ end
462
+ elsif msg.strip.length.positive?
463
+ prefix_str = prefix ? "#{c(:blue)}[#{prefix}]#{c(:reset)} " : ''
464
+ puts "#{prefix_str}#{c(:gray)}#{msg.strip}#{c(:reset)}"
465
+ end
466
+ end
467
+
468
+ def format_structured_log(json, timestamp, prefix: nil)
469
+ level = json['level']
470
+ message = json['message'] || ''
471
+
472
+ return unless should_show_level?(level)
473
+
474
+ prefix_str = prefix ? "#{c(:blue)}[#{prefix}]#{c(:reset)} " : ''
475
+
476
+ case level
477
+ when 'ERROR'
478
+ format_error_log(json, message, timestamp, prefix_str)
479
+ when 'WARN'
480
+ format_warn_log(json, message, timestamp, prefix_str)
481
+ else
482
+ format_info_log(json, message, timestamp, prefix_str)
483
+ end
484
+ end
485
+
486
+ def format_error_log(json, message, timestamp, prefix_str)
487
+ puts "#{prefix_str}#{c(:gray)}#{timestamp}#{c(:reset)} " \
488
+ "#{c(:red)}ERROR#{c(:reset)} #{c(:bold)}#{message}#{c(:reset)}"
489
+ puts " #{c(:dim)}Action:#{c(:reset)} #{json['action']}" if json['action']
490
+ if json['error_class']
491
+ puts " #{c(:dim)}Error:#{c(:reset)} " \
492
+ "#{c(:red)}#{json['error_class']}#{c(:reset)}: #{json['error_message']}"
493
+ end
494
+ puts " #{c(:dim)}Path:#{c(:reset)} #{json['path']}" if json['path']
495
+ format_backtrace(json['backtrace'])
496
+ end
497
+
498
+ def format_warn_log(json, message, timestamp, prefix_str)
499
+ puts "#{prefix_str}#{c(:gray)}#{timestamp}#{c(:reset)} #{c(:yellow)}WARN#{c(:reset)} #{message}"
500
+ if json['error_class']
501
+ puts " #{c(:dim)}Error:#{c(:reset)} " \
502
+ "#{c(:red)}#{json['error_class']}#{c(:reset)}: #{json['error_message']}"
503
+ end
504
+ puts " #{c(:dim)}Path:#{c(:reset)} #{json['path']}" if json['path']
505
+ format_backtrace(json['backtrace'])
506
+ end
507
+
508
+ def format_info_log(json, message, timestamp, prefix_str)
509
+ case message
510
+ when 'Lambda invoked'
511
+ path = strip_namespace(json['path'])
512
+ puts "#{prefix_str}#{c(:gray)}#{timestamp}#{c(:reset)} " \
513
+ "#{c(:cyan)}Started#{c(:reset)} #{c(:bold)}#{json['http_method']}#{c(:reset)} #{path}"
514
+ when 'Request completed'
515
+ path = strip_namespace(json['path'])
516
+ sc = json['status_code'].to_i
517
+ sc_color = status_color(sc)
518
+ puts "#{prefix_str}#{c(:gray)}#{timestamp}#{c(:reset)} " \
519
+ "#{c(:cyan)}Completed#{c(:reset)} #{c(sc_color)}#{sc}#{c(:reset)} #{path}"
520
+ else
521
+ level = json['level']
522
+ level_col = level_color_for(level)
523
+ puts "#{prefix_str}#{c(:gray)}#{timestamp}#{c(:reset)} #{level_col}#{level}#{c(:reset)} #{message}" if level
524
+ end
525
+ end
526
+
527
+ def format_aws_error(json, prefix: nil)
528
+ prefix_str = prefix ? "#{c(:blue)}[#{prefix}]#{c(:reset)} " : ''
529
+ puts "#{prefix_str} #{c(:dim)}Error Type:#{c(:reset)} #{c(:red)}#{json['errorType']}#{c(:reset)}"
530
+ puts "#{prefix_str} #{c(:dim)}Error Message:#{c(:reset)} #{json['errorMessage']}"
531
+ return unless json['stackTrace'].is_a?(Array)
532
+
533
+ puts "#{prefix_str} #{c(:dim)}Stack Trace:#{c(:reset)}"
534
+ json['stackTrace'].first(20).each do |line|
535
+ if line.match?(%r{(controllers|models|lib|helpers)/})
536
+ puts "#{prefix_str} #{c(:yellow)}→#{c(:reset)} #{line}"
537
+ elsif line.include?('/var/task/')
538
+ puts "#{prefix_str} #{c(:cyan)}→#{c(:reset)} #{line}"
539
+ else
540
+ puts "#{prefix_str} #{c(:gray)} #{line}#{c(:reset)}"
541
+ end
542
+ end
543
+ end
544
+
545
+ def format_backtrace(backtrace)
546
+ return unless backtrace.is_a?(Array) && backtrace.any?
547
+
548
+ puts " #{c(:dim)}Backtrace:#{c(:reset)}"
549
+ backtrace.first(15).each do |line|
550
+ if line.match?(%r{(controllers|models|lib|helpers)/})
551
+ puts " #{c(:yellow)}→#{c(:reset)} #{line}"
552
+ else
553
+ puts " #{c(:gray)} #{line}#{c(:reset)}"
554
+ end
555
+ end
556
+ end
557
+
558
+ def should_show_level?(level)
559
+ return true unless level
560
+
561
+ levels = { 'DEBUG' => 1, 'INFO' => 2, 'WARN' => 3, 'ERROR' => 4 }
562
+ (levels[level] || 0) >= (levels[@level] || 2)
563
+ end
564
+
565
+ def level_color_for(level)
566
+ case level
567
+ when 'INFO' then c(:green)
568
+ when 'WARN' then c(:yellow)
569
+ when 'ERROR' then c(:red)
570
+ else c(:gray)
571
+ end
572
+ end
573
+
574
+ def status_color(code)
575
+ if code >= 500
576
+ :red
577
+ elsif code >= 400
578
+ :yellow
579
+ else
580
+ :green
581
+ end
582
+ end
583
+
584
+ def strip_namespace(path)
585
+ return path unless path
586
+
587
+ path.sub(%r{\A/[^/]+}, '')
588
+ end
589
+
590
+ def print_header(lambda_name)
591
+ full_name = "#{@app_name}-#{@env}-#{lambda_name}"
592
+ puts "#{c(:cyan)}═══════════════════════════════════════════════════════════════#{c(:reset)}"
593
+ puts "#{c(:bold)}Lambda Logs: #{c(:magenta)}#{full_name}#{c(:reset)}"
594
+ puts "#{c(:cyan)}═══════════════════════════════════════════════════════════════#{c(:reset)}"
595
+ end
596
+
597
+ def format_timestamp(epoch_ms)
598
+ return '' unless epoch_ms
599
+
600
+ Time.at(epoch_ms / 1000.0).strftime('%H:%M:%S')
601
+ end
602
+
603
+ def extract_time_from_line(line)
604
+ match = line.match(/\A(\d{4}-\d{2}-\d{2}T[\d:]+)/)
605
+ match ? match[1].split('T').last : ''
606
+ end
607
+
608
+ def parse_since(value)
609
+ num = value.to_i
610
+ case value
611
+ when /h\z/ then num * 60 * 60 * 1000
612
+ when /m\z/ then num * 60 * 1000
613
+ when /s\z/ then num * 1000
614
+ else num * 60 * 1000
615
+ end
616
+ end
617
+
618
+ def parse_json(str)
619
+ JSON.parse(str)
620
+ rescue StandardError
621
+ nil
622
+ end
623
+
624
+ def find_infra_dir
625
+ candidates = %w[infrastructure infra]
626
+ candidates.map { |d| File.join(Dir.pwd, d) }.find { |d| Dir.exist?(d) }
627
+ end
628
+
629
+ def c(name)
630
+ @color ? COLORS[name].to_s : ''
631
+ end
632
+ end
633
+ end
634
+ end
@@ -10,10 +10,14 @@ module Belt
10
10
  TEMPLATE_DIR = File.expand_path('../../templates/views', __dir__)
11
11
 
12
12
  def self.run(args)
13
+ force = args.delete('--force') || args.delete('-f')
14
+
13
15
  name = args.shift
14
16
  if name.nil? || name.empty?
15
- puts 'Usage: belt generate views <resource> [field:type ...]'
17
+ puts 'Usage: belt generate views <resource> [field:type ...] [options]'
16
18
  puts "\nGenerates React pages for all REST actions (index, show, new, edit)."
19
+ puts "\nOptions:"
20
+ puts ' --force, -f Overwrite existing files without prompting'
17
21
  puts "\nExamples:"
18
22
  puts ' belt generate views post title:string content:text status:string'
19
23
  puts ' belt generate views comment body:text author:string'
@@ -28,7 +32,7 @@ module Belt
28
32
  # If no fields provided, try to read from contracts.rb
29
33
  fields = read_schema_fields(name) if fields.empty?
30
34
 
31
- new(name, fields).generate
35
+ new(name, fields, force: force).generate
32
36
  end
33
37
 
34
38
  def self.read_schema_fields(name)
@@ -45,20 +49,37 @@ module Belt
45
49
 
46
50
  # Extract fields from model block
47
51
  if content =~ /model :#{singular} do\n(.*?)\n\s*end/m
48
- ::Regexp.last_match(1).scan(/field :(\w+), type: :(\w+)/).except('created_at', 'updated_at')
49
- .map do |n, t|
50
- {
51
- name: n, type: t
52
- }
52
+ block_content = ::Regexp.last_match(1)
53
+ timestamp_fields = %w[created_at updated_at]
54
+
55
+ # Support both formats:
56
+ # field :name, type: :string (legacy)
57
+ # string :name (current schema DSL)
58
+ dsl_types = %w[string text integer number boolean float date datetime]
59
+ dsl_pattern = /(?:#{dsl_types.join('|')}) :(\w+)/
60
+ fields = block_content.scan(/field :(\w+), type: :(\w+)/)
61
+ fields += block_content.scan(dsl_pattern).map do |match|
62
+ field_name = match[0]
63
+ # Extract type from the DSL method name on that line
64
+ type_match = block_content.match(/(\w+) :#{Regexp.escape(field_name)}/)
65
+ [field_name, type_match ? type_match[1] : 'string']
66
+ end
67
+
68
+ fields.filter_map do |n, t|
69
+ next if timestamp_fields.include?(n)
70
+
71
+ { name: n, type: t }
53
72
  end
54
73
  else
55
74
  []
56
75
  end
57
76
  end
58
77
 
59
- def initialize(name, fields)
78
+ def initialize(name, fields, force: false)
60
79
  @name = name.downcase.gsub(/[^a-z0-9_]/, '_')
61
80
  @fields = fields
81
+ @force = force
82
+ @overwrite_all = false
62
83
  @singular_name = Belt::Inflector.singularize(@name)
63
84
  @resource_name = Belt::Inflector.pluralize(@singular_name)
64
85
  @class_name = Belt::Inflector.classify(@singular_name)
@@ -97,8 +118,61 @@ module Belt
97
118
  def write_template(template_name, dest_path)
98
119
  template_path = File.join(TEMPLATE_DIR, template_name)
99
120
  content = ERB.new(File.read(template_path), trim_mode: '-').result(binding)
121
+ existed = File.exist?(dest_path)
122
+
123
+ if existed && !@force && !@overwrite_all
124
+ action = prompt_overwrite(dest_path)
125
+ case action
126
+ when :yes
127
+ # fall through to write
128
+ when :all
129
+ @overwrite_all = true
130
+ # fall through to write
131
+ when :no
132
+ puts " skip #{dest_path}"
133
+ return
134
+ when :quit
135
+ puts "\nAborted."
136
+ exit 1
137
+ end
138
+ end
139
+
100
140
  File.write(dest_path, content)
101
- puts " create #{dest_path}"
141
+ puts " #{existed ? 'overwrite' : 'create'} #{dest_path}"
142
+ end
143
+
144
+ def prompt_overwrite(path)
145
+ return :yes if @overwrite_all
146
+
147
+ print " conflict #{path}\n"
148
+ print " Overwrite #{path}? (enter \"h\" for help) [Ynaqh] "
149
+ $stdout.flush
150
+
151
+ loop do
152
+ answer = $stdin.gets&.strip&.downcase
153
+ case answer
154
+ when '', 'y', 'yes'
155
+ return :yes
156
+ when 'n', 'no'
157
+ return :no
158
+ when 'a', 'all'
159
+ @overwrite_all = true
160
+ return :all
161
+ when 'q', 'quit'
162
+ return :quit
163
+ when 'h', 'help'
164
+ puts ' Y - yes, overwrite this file'
165
+ puts ' n - no, skip this file'
166
+ puts ' a - all, overwrite this and all remaining files'
167
+ puts ' q - quit, abort the generator'
168
+ puts ' h - help, show this help'
169
+ print " Overwrite #{path}? (enter \"h\" for help) [Ynaqh] "
170
+ $stdout.flush
171
+ else
172
+ print ' Please enter Y, n, a, q, or h: '
173
+ $stdout.flush
174
+ end
175
+ end
102
176
  end
103
177
 
104
178
  def inject_routes
@@ -109,6 +183,12 @@ module Belt
109
183
  pages_dir = @resource_name
110
184
  plural_class = @plural_class_name || Belt::Inflector.camelize(@resource_name)
111
185
 
186
+ # Skip route injection if routes for this resource already exist
187
+ if content.include?("path=\"/#{@resource_name}\"")
188
+ puts " skip #{app_jsx} (routes already exist)"
189
+ return
190
+ end
191
+
112
192
  import_lines = [
113
193
  "import #{plural_class}Index from './pages/#{pages_dir}/#{plural_class}Index'",
114
194
  "import #{@class_name}Show from './pages/#{pages_dir}/#{@class_name}Show'",
data/lib/belt/cli.rb CHANGED
@@ -22,6 +22,7 @@ require_relative 'cli/contracts_command'
22
22
  require_relative 'cli/lambda_config_command'
23
23
  require_relative 'cli/tasks_command'
24
24
  require_relative 'cli/console_command'
25
+ require_relative 'cli/logs_command'
25
26
  require_relative 'cli/doctor_command'
26
27
  require_relative 'cli/plugin_command'
27
28
 
@@ -35,6 +36,7 @@ module Belt
35
36
  'contracts' => Belt::CLI::ContractsCommand,
36
37
  'lambda-config' => Belt::CLI::LambdaConfigCommand,
37
38
  %w[console c] => Belt::CLI::ConsoleCommand,
39
+ 'logs' => Belt::CLI::LogsCommand,
38
40
  %w[tasks --tasks -T] => Belt::CLI::TasksCommand,
39
41
  'setup' => Belt::CLI::SetupCommand,
40
42
  'doctor' => Belt::CLI::DoctorCommand,
@@ -116,6 +118,7 @@ module Belt
116
118
 
117
119
  console Start an interactive console (IRB)
118
120
  c Alias for console
121
+ logs [lambda] [-f] [-s 5m] [-e env] View Lambda function logs
119
122
  tasks [-g PATTERN] [-a] List available rake tasks
120
123
  -T [-g PATTERN] [-a] Alias for tasks
121
124
  setup state Create/select S3 state bucket
@@ -520,7 +520,7 @@ module Belt
520
520
 
521
521
  # SchemaBuilder captures request and response model definitions from contracts.rb
522
522
  class SchemaBuilder
523
- SUPPORTED_TYPES = %i[string number integer boolean array object map list].freeze
523
+ SUPPORTED_TYPES = %i[string text number integer boolean date datetime array object map list].freeze
524
524
 
525
525
  attr_reader :request_models, :response_models
526
526
 
@@ -557,7 +557,7 @@ module Belt
557
557
  end
558
558
 
559
559
  class RequestModelBuilder
560
- SUPPORTED_TYPES = %i[string number integer boolean array object map list].freeze
560
+ SUPPORTED_TYPES = %i[string text number integer boolean date datetime array object map list].freeze
561
561
 
562
562
  attr_reader :name, :fields
563
563
 
@@ -590,6 +590,7 @@ module Belt
590
590
 
591
591
  def map_type(dsl_type)
592
592
  case dsl_type
593
+ when :text, :date, :datetime then 'string'
593
594
  when :map then 'object'
594
595
  when :list then 'array'
595
596
  else dsl_type.to_s
@@ -598,7 +599,7 @@ module Belt
598
599
  end
599
600
 
600
601
  class ResponseModelBuilder
601
- SUPPORTED_TYPES = %i[string number integer boolean array object map list].freeze
602
+ SUPPORTED_TYPES = %i[string text number integer boolean date datetime array object map list].freeze
602
603
 
603
604
  attr_reader :name, :contexts, :fields
604
605
 
@@ -636,6 +637,7 @@ module Belt
636
637
 
637
638
  def map_type(dsl_type)
638
639
  case dsl_type
640
+ when :text, :date, :datetime then 'string'
639
641
  when :map then 'object'
640
642
  when :list then 'array'
641
643
  else dsl_type.to_s
@@ -644,7 +646,7 @@ module Belt
644
646
  end
645
647
 
646
648
  class ContextBuilder
647
- SUPPORTED_TYPES = %i[string number integer boolean array object map list].freeze
649
+ SUPPORTED_TYPES = %i[string text number integer boolean date datetime array object map list].freeze
648
650
 
649
651
  attr_reader :name, :fields
650
652
 
@@ -673,6 +675,7 @@ module Belt
673
675
 
674
676
  def map_type(dsl_type)
675
677
  case dsl_type
678
+ when :text, :date, :datetime then 'string'
676
679
  when :map then 'object'
677
680
  when :list then 'array'
678
681
  else dsl_type.to_s
data/lib/belt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Belt
4
- VERSION = '0.2.12'
4
+ VERSION = '0.2.13'
5
5
  end
data/lib/belt.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'activeitem'
3
4
  require_relative 'belt/version'
4
5
  require_relative 'belt/root'
5
6
  require_relative 'belt/configuration'
@@ -6,53 +6,48 @@ module <%= @module_name %>Controllers
6
6
  class <%= @class_name %>sController < ApplicationController
7
7
  # GET /<%= @resource_name %>
8
8
  def index
9
- <%= @resource_name %> = <%= @class_name %>.all
10
- success_response(<%= @resource_name %>: <%= @resource_name %>.map(&:to_h))
9
+ @<%= @resource_name %> = <%= @class_name %>.all
11
10
  end
12
11
 
13
12
  # POST /<%= @resource_name %>
14
13
  def create
15
- <%= @singular_name %> = <%= @class_name %>.new(<%= @fields.map { |f| "#{f[:name]}: params[:#{f[:name]}]" }.join(', ') %>)
14
+ @<%= @singular_name %> = <%= @class_name %>.new(<%= @fields.map { |f| "#{f[:name]}: params[:#{f[:name]}]" }.join(', ') %>)
16
15
 
17
- if <%= @singular_name %>.save
18
- success_response(<%= @singular_name %>: <%= @singular_name %>.to_h)
16
+ if @<%= @singular_name %>.save
17
+ response_status :created
19
18
  else
20
- error_response(<%= @singular_name %>.errors.full_messages.join(', '), 422)
19
+ error_response(@<%= @singular_name %>.errors.full_messages.join(', '), :unprocessable_entity)
21
20
  end
22
21
  end
23
22
 
24
23
  # GET /<%= @resource_name %>/:<%= @singular_name %>_id
25
24
  def show
26
- <%= @singular_name %> = <%= @class_name %>.find(params[:<%= @singular_name %>_id])
27
- return error_response('<%= @class_name %> not found', 404) unless <%= @singular_name %>
28
-
29
- success_response(<%= @singular_name %>: <%= @singular_name %>.to_h)
25
+ @<%= @singular_name %> = <%= @class_name %>.find(params[:<%= @singular_name %>_id])
26
+ return error_response('<%= @class_name %> not found', :not_found) unless @<%= @singular_name %>
30
27
  end
31
28
 
32
29
  # PUT /<%= @resource_name %>/:<%= @singular_name %>_id
33
30
  def update
34
- <%= @singular_name %> = <%= @class_name %>.find(params[:<%= @singular_name %>_id])
35
- return error_response('<%= @class_name %> not found', 404) unless <%= @singular_name %>
31
+ @<%= @singular_name %> = <%= @class_name %>.find(params[:<%= @singular_name %>_id])
32
+ return error_response('<%= @class_name %> not found', :not_found) unless @<%= @singular_name %>
36
33
 
37
34
  attrs = {}
38
35
  <% @fields.each do |field| -%>
39
36
  attrs[:<%= field[:name] %>] = params[:<%= field[:name] %>] if params.key?(:<%= field[:name] %>)
40
37
  <% end -%>
41
38
 
42
- if <%= @singular_name %>.update(attrs)
43
- success_response(<%= @singular_name %>: <%= @singular_name %>.to_h)
44
- else
45
- error_response(<%= @singular_name %>.errors.full_messages.join(', '), 422)
39
+ unless @<%= @singular_name %>.update(attrs)
40
+ error_response(@<%= @singular_name %>.errors.full_messages.join(', '), :unprocessable_entity)
46
41
  end
47
42
  end
48
43
 
49
44
  # DELETE /<%= @resource_name %>/:<%= @singular_name %>_id
50
45
  def destroy
51
46
  <%= @singular_name %> = <%= @class_name %>.find(params[:<%= @singular_name %>_id])
52
- return error_response('<%= @class_name %> not found', 404) unless <%= @singular_name %>
47
+ return error_response('<%= @class_name %> not found', :not_found) unless <%= @singular_name %>
53
48
 
54
49
  <%= @singular_name %>.destroy
55
- success_response(message: '<%= @class_name %> deleted')
50
+ head :no_content
56
51
  end
57
52
  end
58
53
  end
@@ -1,18 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class <%= @class_name %> < ApplicationRecord
4
- <% @fields.each do |field| -%>
5
- attr_accessor :<%= field[:name] %>
6
- <% end -%>
7
-
8
- def to_h
9
- {
10
- id: id,
11
- <% @fields.each do |field| -%>
12
- <%= field[:name] %>: <%= field[:name] %>,
13
- <% end -%>
14
- created_at: created_at,
15
- updated_at: updated_at
16
- }
17
- end
4
+ attr_accessor <%= @fields.map { |f| ":#{f[:name]}" }.join(', ') %>
18
5
  end
@@ -2,6 +2,4 @@
2
2
 
3
3
  source 'https://rubygems.org'
4
4
 
5
- gem 'activeitem'
6
5
  gem 'belt'
7
- gem 'lambda_loadout'
@@ -12,3 +12,6 @@
12
12
  # Lambda build artifacts
13
13
  /lambda/vendor/
14
14
  /lambda/.bundle/
15
+
16
+ # Generated route manifests (belt routes --namespace)
17
+ /lambda/lib/routes/
@@ -1,15 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'belt'
4
- require 'activeitem'
5
4
 
6
5
  include Belt::LambdaHandler
7
6
 
8
- ActiveItem.configure do |config|
9
- config.table_prefix = ENV['APP_NAME']
10
- config.environment = ENV['ENVIRONMENT']
11
- end
12
-
13
7
  require_relative 'lib/routes/api_routes'
14
8
  <% @resources&.each do |r| -%>
15
9
  require_relative 'controllers/<%= @app_name %>/<%= r %>_controller'
@@ -3,16 +3,10 @@
3
3
  # Boot the application. Used by `belt console` and Lambda at runtime.
4
4
 
5
5
  require 'belt'
6
- require 'activeitem'
7
6
 
8
7
  ENV['APP_NAME'] ||= '<%= @app_name %>'
9
8
  ENV['AWS_REGION'] ||= 'us-east-1'
10
9
 
11
- ActiveItem.configure do |config|
12
- config.table_prefix = ENV['APP_NAME']
13
- config.environment = ENV['ENVIRONMENT']
14
- end
15
-
16
10
  # Load lib and models
17
11
  Dir[File.join(__dir__, '..', 'lib', '**', '*.rb')].sort.each { |f| require f }
18
12
 
@@ -1,6 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'activeitem'
4
-
5
3
  class ApplicationRecord < ActiveItem::Base
6
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: belt
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.12
4
+ version: 0.2.13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla
@@ -16,6 +16,9 @@ dependencies:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
18
  version: '0.0'
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: 0.0.13
19
22
  type: :runtime
20
23
  prerelease: false
21
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -23,6 +26,9 @@ dependencies:
23
26
  - - "~>"
24
27
  - !ruby/object:Gem::Version
25
28
  version: '0.0'
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 0.0.13
26
32
  - !ruby/object:Gem::Dependency
27
33
  name: activesupport
28
34
  requirement: !ruby/object:Gem::Requirement
@@ -89,6 +95,7 @@ files:
89
95
  - lib/belt/cli/generate_command.rb
90
96
  - lib/belt/cli/generator_registry.rb
91
97
  - lib/belt/cli/lambda_config_command.rb
98
+ - lib/belt/cli/logs_command.rb
92
99
  - lib/belt/cli/new_command.rb
93
100
  - lib/belt/cli/path_gem_materializer.rb
94
101
  - lib/belt/cli/plugin_command.rb