belt 0.2.9 → 0.2.10

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.
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'open3'
5
+ require 'rubygems/package'
6
+
7
+ module Belt
8
+ module CLI
9
+ # Turns Gemfile `path:` gems into real `.gem` files in vendor/cache and
10
+ # rewrites the Gemfile/lock so Docker `bundle install` produces a normal
11
+ # gem install (with specifications/). Required for Lambda bare `require`
12
+ # without bundler/setup.
13
+ #
14
+ # Only mutates files under +build_dir+ — never the app's real Gemfile.
15
+ class PathGemMaterializer
16
+ PathSource = Struct.new(:remote, :gems, keyword_init: true)
17
+ PathGem = Struct.new(:name, :version, keyword_init: true)
18
+
19
+ def self.materialize!(build_dir, project_root:)
20
+ new(build_dir, project_root: project_root).materialize!
21
+ end
22
+
23
+ def initialize(build_dir, project_root:)
24
+ @build_dir = build_dir
25
+ @project_root = project_root
26
+ @gemfile = File.join(build_dir, 'Gemfile')
27
+ @lockfile = File.join(build_dir, 'Gemfile.lock')
28
+ end
29
+
30
+ # @return [Array<String>] names of gems materialized (empty if none)
31
+ def materialize!
32
+ return [] unless File.exist?(@gemfile) && File.exist?(@lockfile)
33
+
34
+ sources = parse_path_sources(File.read(@lockfile))
35
+ return [] if sources.empty?
36
+
37
+ cache_dir = File.join(@build_dir, 'vendor', 'cache')
38
+ FileUtils.mkdir_p(cache_dir)
39
+
40
+ materialized = []
41
+ sources.each do |source|
42
+ source_path = resolve_remote(source.remote)
43
+ abort "✗ path gem source missing: #{source.remote} (resolved #{source_path})" unless Dir.exist?(source_path)
44
+
45
+ source.gems.each do |gem|
46
+ gem_file = build_gem(source_path, gem)
47
+ dest = File.join(cache_dir, File.basename(gem_file))
48
+ FileUtils.cp(gem_file, dest)
49
+ FileUtils.rm_f(gem_file)
50
+ rewrite_gemfile_path_to_version!(gem.name, gem.version)
51
+ materialized << gem.name
52
+ end
53
+ end
54
+
55
+ return [] if materialized.empty?
56
+
57
+ relock!(materialized)
58
+ materialized
59
+ end
60
+
61
+ private
62
+
63
+ def parse_path_sources(lockfile_content)
64
+ sources = []
65
+ lockfile_content.scan(/^PATH\n remote: (.+)\n specs:\n((?: .+\n)*)/) do |remote, specs_block|
66
+ gems = []
67
+ specs_block.each_line do |line|
68
+ next unless line.match?(/^ \S/)
69
+ next if line.start_with?(' ') # dependency lines are deeper
70
+
71
+ match = line.match(/^ (\S+)\s+\(([^)]+)\)/)
72
+ gems << PathGem.new(name: match[1], version: match[2]) if match
73
+ end
74
+ sources << PathSource.new(remote: remote.strip, gems: gems) if gems.any?
75
+ end
76
+ sources
77
+ end
78
+
79
+ def resolve_remote(remote)
80
+ return remote if remote.start_with?('/')
81
+
82
+ File.expand_path(remote, @project_root)
83
+ end
84
+
85
+ def build_gem(source_path, gem)
86
+ gemspec = find_gemspec(source_path, gem.name)
87
+ abort "✗ No gemspec for path gem #{gem.name} under #{source_path}" unless gemspec
88
+
89
+ Dir.chdir(source_path) do
90
+ spec = Gem::Specification.load(File.basename(gemspec))
91
+ abort "✗ Failed to load #{gemspec}" unless spec
92
+
93
+ if spec.version.to_s != gem.version
94
+ abort "✗ path gem #{gem.name} version mismatch: gemspec #{spec.version}, lock #{gem.version}"
95
+ end
96
+
97
+ # Gem::Package.build writes the .gem into cwd
98
+ built = Gem::Package.build(spec)
99
+ File.expand_path(built)
100
+ end
101
+ end
102
+
103
+ def find_gemspec(source_path, gem_name)
104
+ preferred = File.join(source_path, "#{gem_name}.gemspec")
105
+ return preferred if File.exist?(preferred)
106
+
107
+ Dir.glob(File.join(source_path, '*.gemspec')).first
108
+ end
109
+
110
+ def rewrite_gemfile_path_to_version!(name, version)
111
+ content = File.read(@gemfile)
112
+ new_content = content.lines.map { |line| convert_path_line(line, name, version) }.join
113
+ abort "✗ Could not rewrite path: for gem #{name.inspect} in build Gemfile" if new_content == content
114
+ File.write(@gemfile, new_content)
115
+ end
116
+
117
+ # Converts `gem 'name', path: '...'` (and variants with other kwargs) into
118
+ # a version-pinned line. Leaves non-matching lines alone.
119
+ def convert_path_line(line, name, version)
120
+ return line unless line.match?(/gem\s+(['"])#{Regexp.escape(name)}\1/)
121
+ return line unless line.match?(/\bpath:\s*/)
122
+
123
+ quote = line[/gem\s+(['"])/, 1] || "'"
124
+ cleaned = line.sub(/,?\s*path:\s*['"][^'"]+['"]/, '')
125
+ cleaned.sub(/gem\s+(['"])#{Regexp.escape(name)}\1/) do
126
+ "gem #{quote}#{name}#{quote}, #{quote}#{version}#{quote}"
127
+ end
128
+ end
129
+
130
+ def relock!(gem_names)
131
+ Dir.chdir(@build_dir) do
132
+ # vendor/cache has the built .gem — Bundler resolves the unpublished version
133
+ output, status = Open3.capture2e('bundle', 'lock', '--update', *gem_names)
134
+ return if status.success?
135
+
136
+ abort "✗ Failed to re-lock after materializing path gems (#{gem_names.join(', ')}):\n#{output}"
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -38,11 +38,12 @@ module Belt
38
38
  end
39
39
  end
40
40
 
41
- def initialize(args = [])
41
+ def initialize(args = [], quiet: false)
42
42
  @app_name = detect_app_name
43
43
  @env_name = nil
44
44
  @custom_bucket = nil
45
45
  @select_mode = false
46
+ @quiet = quiet
46
47
 
47
48
  parse_args(args)
48
49
 
@@ -67,7 +68,7 @@ module Belt
67
68
  @bucket_name = interactive_bucket_selection if @select_mode
68
69
  setup_or_verify_bucket
69
70
  apply_lifecycle(@bucket_name)
70
- puts ' ensure lifecycle rules (90-day noncurrent expiration)'
71
+ say ' ensure lifecycle rules (90-day noncurrent expiration)'
71
72
  update_backend_config
72
73
  print_success_message
73
74
  end
@@ -81,12 +82,12 @@ module Belt
81
82
  end
82
83
 
83
84
  def verify_existing_bucket
84
- puts "Found existing bucket: #{@bucket_name}"
85
+ say "Found existing bucket: #{@bucket_name}"
85
86
  audit = audit_bucket_security(@bucket_name)
86
- print_security_audit(audit)
87
+ print_security_audit(audit) unless @quiet
87
88
 
88
89
  if audit.values.all?
89
- puts "\n✓ Bucket '#{@bucket_name}' passes all security checks"
90
+ say "\n✓ Bucket '#{@bucket_name}' passes all security checks"
90
91
  else
91
92
  prompt_and_harden(audit)
92
93
  end
@@ -94,6 +95,12 @@ module Belt
94
95
 
95
96
  def prompt_and_harden(audit)
96
97
  puts "\n⚠ Bucket '#{@bucket_name}' has security issues."
98
+ # Auto-harden during belt new (quiet); interactive otherwise
99
+ if @quiet
100
+ harden_bucket(@bucket_name, audit)
101
+ return
102
+ end
103
+
97
104
  print "\nApply security hardening? [Y/n] "
98
105
  response = $stdin.gets&.strip&.downcase
99
106
  if response.nil? || response.empty? || response == 'y'
@@ -105,13 +112,15 @@ module Belt
105
112
  end
106
113
 
107
114
  def create_new_bucket
108
- puts "Creating state bucket: #{@bucket_name} (#{@region})"
115
+ say "Creating state bucket: #{@bucket_name} (#{@region})"
109
116
  create_bucket(@bucket_name)
110
- puts " create s3://#{@bucket_name}"
117
+ say " create s3://#{@bucket_name}"
111
118
  harden_bucket(@bucket_name, {})
112
119
  end
113
120
 
114
121
  def print_success_message
122
+ return if @quiet
123
+
115
124
  puts "\n✓ State bucket '#{@bucket_name}' is ready!"
116
125
  if @env_name
117
126
  puts "\n cd infrastructure/#{@env_name} && terraform init"
@@ -120,6 +129,9 @@ module Belt
120
129
  end
121
130
  end
122
131
 
132
+ # Expose for quiet callers (e.g. belt new) that print their own summary
133
+ attr_reader :bucket_name
134
+
123
135
  private
124
136
 
125
137
  def parse_args(args)
@@ -138,8 +150,15 @@ module Belt
138
150
  end
139
151
  end
140
152
 
153
+ # One shared state bucket per AWS account (all belt apps use it).
154
+ # Account ID keeps the name globally unique — S3 names are a global namespace.
155
+ # Pattern: belt-terraform-state-<account_id>
141
156
  def resolve_bucket_name
142
- @custom_bucket || 'belt-terraform-state'
157
+ return @custom_bucket if @custom_bucket
158
+ return "belt-terraform-state-#{@aws_account_id}" if @aws_account_id
159
+
160
+ # Placeholder until credentials are available (belt setup state re-resolves)
161
+ 'belt-terraform-state'
143
162
  end
144
163
 
145
164
  # --- Interactive selection ---
@@ -238,9 +257,11 @@ module Belt
238
257
  end
239
258
 
240
259
  def run!(*args)
241
- return if system(*args)
260
+ output, status = Open3.capture2e(*args)
261
+ return if status.success?
242
262
 
243
263
  puts "\n✗ Command failed: #{args.shelljoin}"
264
+ puts output.strip unless output.strip.empty?
244
265
  exit 1
245
266
  end
246
267
 
@@ -249,6 +270,10 @@ module Belt
249
270
  status.success? ? output : nil
250
271
  end
251
272
 
273
+ def say(message)
274
+ puts message unless @quiet
275
+ end
276
+
252
277
  # --- Backend config ---
253
278
 
254
279
  def update_backend_config
@@ -268,7 +293,7 @@ module Belt
268
293
  updated = content.gsub(/bucket\s*=\s*"[^"]+"/, "bucket = \"#{@bucket_name}\"")
269
294
  if updated != content
270
295
  File.write(backend_file, updated)
271
- puts " update #{backend_file} → bucket = \"#{@bucket_name}\""
296
+ say " update #{backend_file} → bucket = \"#{@bucket_name}\""
272
297
  end
273
298
  end
274
299
  end
data/lib/belt/cli.rb CHANGED
@@ -21,6 +21,7 @@ require_relative 'cli/routes_command'
21
21
  require_relative 'cli/lambda_config_command'
22
22
  require_relative 'cli/tasks_command'
23
23
  require_relative 'cli/console_command'
24
+ require_relative 'cli/doctor_command'
24
25
 
25
26
  module Belt
26
27
  module CLI
@@ -33,6 +34,7 @@ module Belt
33
34
  %w[console c] => Belt::CLI::ConsoleCommand,
34
35
  %w[tasks --tasks -T] => Belt::CLI::TasksCommand,
35
36
  'setup' => Belt::CLI::SetupCommand,
37
+ 'doctor' => Belt::CLI::DoctorCommand,
36
38
  'deploy' => Belt::CLI::DeployCommand,
37
39
  'frontend' => Belt::CLI::FrontendEnvCommand,
38
40
  %w[server s] => Belt::CLI::ServerCommand,
@@ -114,6 +116,7 @@ module Belt
114
116
  setup state Create/select S3 state bucket
115
117
  setup tables <env> Generate DynamoDB tables from schema
116
118
  setup frontend <env> Generate S3 + CloudFront infrastructure
119
+ doctor Check system dependencies and AWS config
117
120
  init [environment] <env> terraform init for environment
118
121
  plan [environment] <env> terraform plan for environment
119
122
  apply [environment] <env> terraform apply for environment
@@ -133,6 +136,7 @@ module Belt
133
136
 
134
137
  Examples:
135
138
  belt new blog --frontend react
139
+ belt new blog --frontend react -v # list every created file
136
140
  belt generate scaffold post title:string content:text status:string
137
141
  belt destroy scaffold post
138
142
  belt generate frontend react
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Belt
4
+ # Runtime configuration for Belt apps (set in lambda/config/environment.rb).
5
+ #
6
+ # Belt.configure do |config|
7
+ # config.default_format = :json # or :html
8
+ # end
9
+ #
10
+ # Note: infrastructure/<env>/belt.rb uses a separate sandboxed DSL for CLI
11
+ # deploy/backup settings — it does not share this object.
12
+ class Configuration
13
+ VALID_FORMATS = %i[json html].freeze
14
+
15
+ def initialize
16
+ @default_format = :json
17
+ end
18
+
19
+ attr_reader :default_format
20
+
21
+ def default_format=(value)
22
+ format = value.to_sym
23
+ unless VALID_FORMATS.include?(format)
24
+ raise ArgumentError, "default_format must be :json or :html (got #{value.inspect})"
25
+ end
26
+
27
+ @default_format = format
28
+ end
29
+ end
30
+ end
@@ -6,23 +6,90 @@ module Belt
6
6
  # Gem-embedded welcome controller that serves the default "it works!" page.
7
7
  # Resolved via the ActionRouter's Belt:: namespace fallback when no app-defined
8
8
  # WelcomeController exists. Gets replaced once the user defines their own root route.
9
+ #
10
+ # Dual format:
11
+ # - Browser / no Accept preference → HTML hero + stack check
12
+ # - Accept: application/json (SPA / apiClient) → JSON payload for the React shell
13
+ #
14
+ # The hero image (lib/belt/assets/belt-default.jpg) is the single source of truth —
15
+ # HTML embeds it, and JSON returns the same bytes as a data URI so the SPA matches.
9
16
  class WelcomeController < BeltController::Base
10
17
  skip_before_action :authenticate!
11
18
 
12
19
  ASSETS_DIR = File.expand_path('../assets', __dir__)
20
+ # Scaffold gives model + controller + routes + schema (not model-only).
21
+ SCAFFOLD_HINT_CMD = 'belt generate scaffold Post title content:text'
13
22
 
14
23
  def show
15
24
  @title = ENV.fetch('WELCOME_TITLE', 'Welcome to Belt')
16
25
  @subtitle = ENV.fetch('WELCOME_SUBTITLE', 'API Gateway → Lambda → DynamoDB — all connected.')
17
- @css = welcome_css
18
- @background_image = background_image_base64
19
26
  @dynamodb_connected = dynamodb_connected?
27
+ @scaffold_hint_cmd = SCAFFOLD_HINT_CMD
28
+
29
+ return success_response(welcome_payload) if wants_json?
20
30
 
31
+ @css = welcome_css
32
+ @background_image = background_image_base64
21
33
  render
22
34
  end
23
35
 
24
36
  private
25
37
 
38
+ def welcome_payload
39
+ {
40
+ title: @title,
41
+ subtitle: @subtitle,
42
+ # Same gem asset as the HTML welcome page — edit belt-default.jpg once.
43
+ background_image: "data:image/jpeg;base64,#{background_image_base64}",
44
+ stack: {
45
+ api_gateway: true,
46
+ lambda: true,
47
+ dynamodb: @dynamodb_connected
48
+ },
49
+ # One tip only (was duplicated as dynamodb_hint + first next_step).
50
+ next_steps: [
51
+ "Scaffold a resource: #{SCAFFOLD_HINT_CMD}",
52
+ 'Deploy: belt deploy',
53
+ 'This page will be replaced once you define your own root route.'
54
+ ]
55
+ }
56
+ end
57
+
58
+ # SPA apiClient and fetch(..., { headers: { Accept: "application/json" } })
59
+ def wants_json?
60
+ return true if params['format'].to_s == 'json'
61
+
62
+ accept = header_value('Accept') || header_value('accept') || ''
63
+ return false if accept.empty?
64
+
65
+ # Prefer JSON when it's listed and not beaten by an explicit HTML preference
66
+ json_q = accept_quality(accept, 'application/json')
67
+ html_q = accept_quality(accept, 'text/html')
68
+ json_q.positive? && json_q >= html_q
69
+ end
70
+
71
+ def header_value(name)
72
+ headers = event.is_a?(Hash) ? event['headers'] : nil
73
+ return nil unless headers.is_a?(Hash)
74
+
75
+ headers[name] || headers[name.downcase] || headers[name.split('-').map(&:capitalize).join('-')]
76
+ end
77
+
78
+ def accept_quality(accept, media_type)
79
+ accept.split(',').each do |part|
80
+ type, *params = part.strip.split(';').map(&:strip)
81
+ # Exact type only — do not treat */* as JSON (curl/browsers send that by default)
82
+ next unless type == media_type
83
+
84
+ q = 1.0
85
+ params.each do |p|
86
+ q = p.split('=', 2).last.to_f if p.start_with?('q=')
87
+ end
88
+ return q
89
+ end
90
+ 0.0
91
+ end
92
+
26
93
  def dynamodb_connected?
27
94
  return false unless defined?(Aws::DynamoDB::Client)
28
95
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'error_logging'
4
4
  require_relative 'cors_origin'
5
+ require_relative '../http_status'
5
6
 
6
7
  module Belt
7
8
  module Helpers
@@ -9,8 +10,9 @@ module Belt
9
10
  def cors_headers(event = nil)
10
11
  event = @event if event.nil? && instance_variable_defined?(:@event)
11
12
  origin = CorsOrigin.resolve_origin(CorsOrigin.origin_from_event(event))
13
+ allow_headers = 'Content-Type,Accept,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'
12
14
  headers = {
13
- 'Access-Control-Allow-Headers' => 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
15
+ 'Access-Control-Allow-Headers' => allow_headers,
14
16
  'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
15
17
  'Access-Control-Max-Age' => '300',
16
18
  'Content-Type' => 'application/json'
@@ -19,16 +21,18 @@ module Belt
19
21
  headers
20
22
  end
21
23
 
24
+ # JSON success. Status accepts Integer or Symbol (:created, :ok, …).
22
25
  def success_response(body, status_code = 200)
23
- { statusCode: status_code, headers: cors_headers, body: JSON.generate(body) }
26
+ { statusCode: resolve_status(status_code), headers: cors_headers, body: JSON.generate(body) }
24
27
  end
25
28
 
29
+ # JSON error. Status accepts Integer or Symbol (:unprocessable_entity, :not_found, …).
26
30
  def error_response(message, status_code = 400, error_details = nil)
27
31
  body = { error: message }
28
32
  if error_details
29
33
  body[:details] = error_details.is_a?(Hash) ? error_details : { message: error_details.to_s }
30
34
  end
31
- { statusCode: status_code, headers: cors_headers, body: JSON.generate(body) }
35
+ { statusCode: resolve_status(status_code), headers: cors_headers, body: JSON.generate(body) }
32
36
  end
33
37
 
34
38
  def html_response(html, status_code = 200)
@@ -36,7 +40,30 @@ module Belt
36
40
  origin = CorsOrigin.resolve_origin(CorsOrigin.origin_from_event(event))
37
41
  headers = { 'Content-Type' => 'text/html; charset=utf-8' }
38
42
  headers['Access-Control-Allow-Origin'] = origin if origin
39
- { statusCode: status_code, headers: headers, body: html }
43
+ { statusCode: resolve_status(status_code), headers: headers, body: html }
44
+ end
45
+
46
+ # Empty-body response (Rails-style). Useful for destroy / create-without-body.
47
+ #
48
+ # head :created # 201, empty body
49
+ # head :no_content # 204
50
+ # head 201
51
+ #
52
+ def head(status = :no_content)
53
+ { statusCode: resolve_status(status), headers: cors_headers, body: '' }
54
+ end
55
+
56
+ # Set the status used by implicit assigns / HTML render (when you don't
57
+ # call success_response / render yourself).
58
+ #
59
+ # def create
60
+ # @post = Post.create!(...)
61
+ # response_status :created
62
+ # end
63
+ # # → 201 + { post: {...} }
64
+ #
65
+ def response_status(status)
66
+ @__response_status = resolve_status(status)
40
67
  end
41
68
 
42
69
  def handle_error_and_respond(error, message, context = {}, status_code = 500)
@@ -56,6 +83,10 @@ module Belt
56
83
 
57
84
  private
58
85
 
86
+ def resolve_status(status)
87
+ Belt::HttpStatus.resolve(status)
88
+ end
89
+
59
90
  def verbose_errors_enabled?
60
91
  env = ENV['ENVIRONMENT']&.downcase || ''
61
92
  env.start_with?('dev') || env == 'local' || env == 'test'
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Belt
4
+ # Rails/Rack-style symbolic HTTP status codes.
5
+ # Mirrors the common subset of Rack::Utils::SYMBOL_TO_STATUS_CODE so apps
6
+ # can write `head :created` / `success_response(body, :created)` without
7
+ # taking a rack dependency.
8
+ module HttpStatus
9
+ SYMBOL_TO_STATUS_CODE = {
10
+ continue: 100,
11
+ switching_protocols: 101,
12
+ processing: 102,
13
+ ok: 200,
14
+ created: 201,
15
+ accepted: 202,
16
+ non_authoritative_information: 203,
17
+ no_content: 204,
18
+ reset_content: 205,
19
+ partial_content: 206,
20
+ multi_status: 207,
21
+ already_reported: 208,
22
+ im_used: 226,
23
+ multiple_choices: 300,
24
+ moved_permanently: 301,
25
+ found: 302,
26
+ see_other: 303,
27
+ not_modified: 304,
28
+ use_proxy: 305,
29
+ temporary_redirect: 307,
30
+ permanent_redirect: 308,
31
+ bad_request: 400,
32
+ unauthorized: 401,
33
+ payment_required: 402,
34
+ forbidden: 403,
35
+ not_found: 404,
36
+ method_not_allowed: 405,
37
+ not_acceptable: 406,
38
+ proxy_authentication_required: 407,
39
+ request_timeout: 408,
40
+ conflict: 409,
41
+ gone: 410,
42
+ length_required: 411,
43
+ precondition_failed: 412,
44
+ payload_too_large: 413,
45
+ uri_too_long: 414,
46
+ unsupported_media_type: 415,
47
+ range_not_satisfiable: 416,
48
+ expectation_failed: 417,
49
+ unprocessable_entity: 422,
50
+ locked: 423,
51
+ failed_dependency: 424,
52
+ upgrade_required: 426,
53
+ precondition_required: 428,
54
+ too_many_requests: 429,
55
+ request_header_fields_too_large: 431,
56
+ unavailable_for_legal_reasons: 451,
57
+ internal_server_error: 500,
58
+ not_implemented: 501,
59
+ bad_gateway: 502,
60
+ service_unavailable: 503,
61
+ gateway_timeout: 504,
62
+ http_version_not_supported: 505,
63
+ variant_also_negotiates: 506,
64
+ insufficient_storage: 507,
65
+ loop_detected: 508,
66
+ not_extended: 510,
67
+ network_authentication_required: 511
68
+ }.freeze
69
+
70
+ module_function
71
+
72
+ # Resolve a status to an integer code.
73
+ # Accepts Integer (200), String ("201"), or Symbol (:created).
74
+ def resolve(status)
75
+ case status
76
+ when Integer
77
+ status
78
+ when String
79
+ Integer(status)
80
+ when Symbol
81
+ SYMBOL_TO_STATUS_CODE.fetch(status) do
82
+ raise ArgumentError, "Unknown HTTP status symbol: #{status.inspect}"
83
+ end
84
+ else
85
+ raise ArgumentError, "status must be Integer, String, or Symbol (got #{status.class})"
86
+ end
87
+ end
88
+ end
89
+ end
@@ -54,6 +54,7 @@ module Belt
54
54
  # render layout: false — skip layout (default: no layout)
55
55
  #
56
56
  def render(options = {})
57
+ # Status may be Integer or Symbol (:created, :ok, …)
57
58
  status = options.fetch(:status, 200)
58
59
 
59
60
  html = if options.key?(:inline)
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.9'
4
+ VERSION = '0.2.10'
5
5
  end
@@ -12,39 +12,39 @@
12
12
  <div class="overlay">
13
13
  <h1><%= h(@title) %></h1>
14
14
  <p class="subtitle"><%= h(@subtitle) %></p>
15
- </div>
16
- </div>
17
- <div class="container">
18
- <div class="stack-check">
19
- <div class="check-item check-pass">
20
- <span class="icon">✓</span>
21
- <span>API Gateway</span>
22
- </div>
23
- <div class="arrow">→</div>
24
- <div class="check-item check-pass">
25
- <span class="icon">✓</span>
26
- <span>Lambda</span>
15
+
16
+ <div class="stack-check">
17
+ <div class="check-item check-pass">
18
+ <span class="icon">✓</span>
19
+ <span>API Gateway</span>
20
+ </div>
21
+ <div class="arrow">→</div>
22
+ <div class="check-item check-pass">
23
+ <span class="icon">✓</span>
24
+ <span>Lambda</span>
25
+ </div>
26
+ <div class="arrow">→</div>
27
+ <%- if @dynamodb_connected -%>
28
+ <div class="check-item check-pass">
29
+ <span class="icon">✓</span>
30
+ <span>DynamoDB</span>
31
+ </div>
32
+ <%- else -%>
33
+ <div class="check-item check-neutral">
34
+ <span class="icon">○</span>
35
+ <span>DynamoDB</span>
36
+ </div>
37
+ <%- end -%>
27
38
  </div>
28
- <div class="arrow">→</div>
29
- <%- if @dynamodb_connected -%>
30
- <div class="check-item check-pass">
31
- <span class="icon">✓</span>
32
- <span>DynamoDB</span>
39
+
40
+ <div class="next-steps">
41
+ <h2>Next Steps</h2>
42
+ <ol>
43
+ <li>Scaffold a resource: <code><%= h(@scaffold_hint_cmd) %></code></li>
44
+ <li>Deploy: <code>belt deploy</code></li>
45
+ <li>This page will be replaced once you define your own root route.</li>
46
+ </ol>
33
47
  </div>
34
- <%- else -%>
35
- <div class="check-item check-warn">
36
- <span class="icon">⚠</span>
37
- <span>DynamoDB</span>
38
- </div>
39
- <%- end -%>
40
- </div>
41
- <div class="next-steps">
42
- <h2>Next Steps</h2>
43
- <ol>
44
- <li>Generate a resource: <code>belt g resource post title:string body:text</code></li>
45
- <li>Deploy: <code>belt deploy</code></li>
46
- <li>This page will be replaced once you define your own root route.</li>
47
- </ol>
48
48
  </div>
49
49
  </div>
50
50
  </body>