belt 0.1.4 → 0.1.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.
@@ -0,0 +1,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'json'
5
+ require_relative 'app_detection'
6
+ require_relative 'terraform_command'
7
+
8
+ module Belt
9
+ module CLI
10
+ class ServerCommand
11
+ include AppDetection
12
+
13
+ DEFAULT_PORT = 3000
14
+
15
+ def self.run(args)
16
+ port = DEFAULT_PORT
17
+ open_browser = false
18
+
19
+ i = 0
20
+ while i < args.length
21
+ case args[i]
22
+ when '-p', '--port'
23
+ i += 1
24
+ port = args[i].to_i
25
+ when /^--port=/
26
+ port = args[i].split('=', 2).last.to_i
27
+ when '-o', '--open'
28
+ open_browser = true
29
+ when '-h', '--help'
30
+ puts help_text
31
+ exit 0
32
+ end
33
+ i += 1
34
+ end
35
+
36
+ new(port: port, open_browser: open_browser).run
37
+ end
38
+
39
+ def self.help_text
40
+ <<~HELP
41
+ Start a local development server for the frontend.
42
+
43
+ Usage: belt server [options]
44
+ belt s [options]
45
+
46
+ Options:
47
+ -p, --port PORT Port to serve on (default: #{DEFAULT_PORT})
48
+ -o, --open Open browser after starting
49
+ -h, --help Show this help
50
+
51
+ Behavior:
52
+ • If frontend/ exists → runs the frontend dev server (npm run dev)
53
+ Automatically sets VITE_API_URL from terraform outputs if deployed.
54
+ • If no frontend → serves the welcome page via a local HTTP server
55
+ After deploy, shows live API URL and deployment status.
56
+
57
+ Note: The backend is serverless (AWS Lambda). Use `belt deploy` to deploy
58
+ your backend to AWS. Local frontend development automatically points to
59
+ your deployed API via VITE_API_URL.
60
+
61
+ Examples:
62
+ belt server # Start on port #{DEFAULT_PORT}
63
+ belt s -p 4000 # Start on port 4000
64
+ belt s --open # Start and open browser
65
+ HELP
66
+ end
67
+
68
+ def initialize(port:, open_browser: false)
69
+ @port = port
70
+ @open_browser = open_browser
71
+ @app_name = detect_app_name
72
+ @api_url = detect_api_url
73
+ end
74
+
75
+ def run
76
+ if Dir.exist?('frontend') && File.exist?('frontend/package.json')
77
+ run_frontend_dev_server
78
+ else
79
+ run_welcome_server
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def run_frontend_dev_server
86
+ puts "🚀 Starting frontend dev server on port #{@port}..."
87
+ if @api_url
88
+ puts " Backend API: #{@api_url}"
89
+ else
90
+ puts ' Backend is serverless — deploy with `belt deploy` to set up AWS resources.'
91
+ end
92
+ puts ''
93
+
94
+ open_browser_later if @open_browser
95
+
96
+ env = { 'PORT' => @port.to_s }
97
+ env['VITE_API_URL'] = @api_url if @api_url
98
+
99
+ # Prefer the dev script with the port flag for Vite-based setups
100
+ Dir.chdir('frontend') do
101
+ exec(env, 'npx', 'vite', '--port', @port.to_s)
102
+ end
103
+ end
104
+
105
+ def run_welcome_server
106
+ if @api_url
107
+ puts "🚀 Serving Belt welcome page on http://localhost:#{@port}"
108
+ puts " Backend API: #{@api_url}"
109
+ puts ''
110
+ puts ' Tip: Run `belt generate frontend react` to scaffold a frontend app.'
111
+ puts ''
112
+ else
113
+ puts "🚀 Serving Belt welcome page on http://localhost:#{@port}"
114
+ puts ' No deployment detected — showing pre-deploy welcome page.'
115
+ puts ' Backend is serverless — deploy with `belt deploy` to set up AWS resources.'
116
+ puts ''
117
+ puts ' Tip: Run `belt generate frontend react` to scaffold a frontend app.'
118
+ puts ''
119
+ end
120
+
121
+ require 'webrick'
122
+
123
+ server = WEBrick::HTTPServer.new(Port: @port, Logger: WEBrick::Log.new('/dev/null'), AccessLog: [])
124
+
125
+ server.mount_proc '/' do |_req, res|
126
+ res['Content-Type'] = 'text/html; charset=utf-8'
127
+ res.body = welcome_html
128
+ end
129
+
130
+ open_browser_later if @open_browser
131
+
132
+ trap('INT') { server.shutdown }
133
+ trap('TERM') { server.shutdown }
134
+
135
+ server.start
136
+ end
137
+
138
+ def gem_assets_dir
139
+ @gem_assets_dir ||= File.expand_path('../assets', __dir__)
140
+ end
141
+
142
+ def welcome_html
143
+ title = ENV.fetch('WELCOME_TITLE', 'Welcome to Belt')
144
+ subtitle = ENV.fetch('WELCOME_SUBTITLE', 'Your app is ready. Deploy to see it live on AWS.')
145
+
146
+ css = load_asset('welcome.css') || ''
147
+ image_b64 = load_image_base64('belt-default.jpg') || ''
148
+
149
+ if @api_url
150
+ deployed_html(css: css, image_b64: image_b64)
151
+ else
152
+ pre_deploy_html(title: title, subtitle: subtitle, css: css, image_b64: image_b64)
153
+ end
154
+ end
155
+
156
+ def load_asset(filename)
157
+ path = File.join(gem_assets_dir, filename)
158
+ File.exist?(path) ? File.read(path) : nil
159
+ end
160
+
161
+ def load_image_base64(filename)
162
+ path = File.join(gem_assets_dir, filename)
163
+ File.exist?(path) ? Base64.strict_encode64(File.binread(path)) : nil
164
+ end
165
+
166
+ def deployed_html(css:, image_b64:)
167
+ <<~HTML
168
+ <!DOCTYPE html>
169
+ <html lang="en">
170
+ <head>
171
+ <meta charset="UTF-8">
172
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
173
+ <title>#{@app_name} — Deployed</title>
174
+ <style>#{css}</style>
175
+ </head>
176
+ <body>
177
+ <div class="hero">
178
+ <img src="data:image/jpeg;base64,#{image_b64}" alt="Belt" class="hero-bg" />
179
+ <div class="overlay">
180
+ <h1>#{@app_name}</h1>
181
+ <p class="subtitle">Deployed and running on AWS</p>
182
+ </div>
183
+ </div>
184
+ <div class="container">
185
+ <div class="next-steps">
186
+ <h2>Next Steps</h2>
187
+ <ol>
188
+ <li>View your live app: <a href="#{@api_url}">#{@api_url}</a> (#{@deploy_env || 'dev'})</li>
189
+ <li>Generate a resource: <code>belt g resource post title:string body:text</code></li>
190
+ <li>Set up a frontend: <code>belt generate frontend react</code></li>
191
+ </ol>
192
+ </div>
193
+ </div>
194
+ </body>
195
+ </html>
196
+ HTML
197
+ end
198
+
199
+ def pre_deploy_html(title:, subtitle:, css:, image_b64:)
200
+ <<~HTML
201
+ <!DOCTYPE html>
202
+ <html lang="en">
203
+ <head>
204
+ <meta charset="UTF-8">
205
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
206
+ <title>#{title}</title>
207
+ <style>#{css}</style>
208
+ </head>
209
+ <body>
210
+ <div class="hero">
211
+ <img src="data:image/jpeg;base64,#{image_b64}" alt="Belt" class="hero-bg" />
212
+ <div class="overlay">
213
+ <h1>#{title}</h1>
214
+ <p class="subtitle">#{subtitle}</p>
215
+ </div>
216
+ </div>
217
+ <div class="container">
218
+ <div class="info">
219
+ <p><strong>App:</strong> #{@app_name}</p>
220
+ <p><strong>Status:</strong> Not yet deployed</p>
221
+ </div>
222
+ <div class="next-steps">
223
+ <h2>Next Steps</h2>
224
+ <ol>
225
+ <li>Generate a resource: <code>belt g resource post title:string body:text</code></li>
226
+ <li>Deploy to AWS: <code>belt deploy</code></li>
227
+ <li>Set up a frontend: <code>belt generate frontend react</code></li>
228
+ </ol>
229
+ </div>
230
+ </div>
231
+ </body>
232
+ </html>
233
+ HTML
234
+ end
235
+
236
+ # Reads terraform outputs to find the deployed API URL.
237
+ # Checks all available environments, preferring BELT_ENV.
238
+ def detect_api_url
239
+ infra_dir = TerraformCommand.find_infrastructure_dir
240
+ return nil unless infra_dir
241
+
242
+ # Prefer BELT_ENV, then try each environment
243
+ envs = TerraformCommand.list_environments
244
+ preferred = ENV.fetch('BELT_ENV', nil)
245
+ envs.unshift(envs.delete(preferred)) if preferred && envs.include?(preferred)
246
+
247
+ envs.each do |env|
248
+ env_dir = File.join(infra_dir, env)
249
+ next unless Dir.exist?(env_dir)
250
+ next unless File.exist?(File.join(env_dir, '.terraform'))
251
+
252
+ url = read_api_url_from_outputs(env_dir)
253
+ if url
254
+ @deploy_env = env
255
+ return url
256
+ end
257
+ end
258
+
259
+ nil
260
+ end
261
+
262
+ def read_api_url_from_outputs(env_dir)
263
+ output = nil
264
+ Dir.chdir(env_dir) do
265
+ # Use Open3 to check exit status — terraform writes errors to stdout too
266
+ require 'open3'
267
+ output, status = Open3.capture2('terraform', 'output', '-json', err: File::NULL)
268
+ return nil unless status.success?
269
+ end
270
+ return nil if output.nil? || output.strip.empty?
271
+
272
+ data = JSON.parse(output.strip)
273
+
274
+ # api_url output is a map like { "app_name" => "https://..." }
275
+ if data['api_url'] && data['api_url']['value']
276
+ value = data['api_url']['value']
277
+ case value
278
+ when String
279
+ value
280
+ when Hash
281
+ # Take the first URL from the map
282
+ value.values.first
283
+ end
284
+ end
285
+ rescue JSON::ParserError, Errno::ENOENT
286
+ nil
287
+ end
288
+
289
+ def open_browser_later
290
+ Thread.new do
291
+ sleep 1.5
292
+ url = "http://localhost:#{@port}"
293
+ if RUBY_PLATFORM.include?('darwin')
294
+ system('open', url)
295
+ elsif RUBY_PLATFORM.include?('linux')
296
+ system('xdg-open', url, out: File::NULL, err: File::NULL)
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end
302
+ end
@@ -52,10 +52,18 @@ module Belt
52
52
 
53
53
  def run_state_setup
54
54
  unless aws_configured?
55
- puts ' AWS credentials not configured. Set AWS_PROFILE or configure aws sso login.'
55
+ if @aws_error&.include?('ForbiddenException') || @aws_error&.include?('AccessDenied')
56
+ puts "✗ AWS credentials found but access denied — check your profile/role configuration."
57
+ puts " #{@aws_error}"
58
+ else
59
+ puts '✗ AWS credentials not configured. Set AWS_PROFILE or configure aws sso login.'
60
+ end
56
61
  exit 1
57
62
  end
58
63
 
64
+ # Re-resolve bucket name with account ID suffix now that we have credentials
65
+ @bucket_name = resolve_bucket_name unless @custom_bucket
66
+
59
67
  @bucket_name = interactive_bucket_selection if @select_mode
60
68
  setup_or_verify_bucket
61
69
  apply_lifecycle(@bucket_name)
@@ -133,10 +141,14 @@ module Belt
133
141
  def resolve_bucket_name
134
142
  if @custom_bucket
135
143
  @custom_bucket
136
- elsif @env_name
137
- s3_safe_name("#{@app_name}-terraform-state-#{@env_name}")
138
144
  else
139
- s3_safe_name("#{@app_name}-terraform-state")
145
+ base = if @env_name
146
+ "#{@app_name}-terraform-state-#{@env_name}"
147
+ else
148
+ "#{@app_name}-terraform-state"
149
+ end
150
+ base = "#{base}-#{@aws_account_id}-#{@region}" if @aws_account_id
151
+ s3_safe_name(base)
140
152
  end
141
153
  end
142
154
 
@@ -184,11 +196,33 @@ module Belt
184
196
  # --- AWS operations ---
185
197
 
186
198
  def aws_configured?
187
- system('aws', 'sts', 'get-caller-identity', out: File::NULL, err: File::NULL)
199
+ output, status = Open3.capture2e('aws', 'sts', 'get-caller-identity')
200
+ if status.success?
201
+ data = JSON.parse(output) rescue {}
202
+ @aws_account_id = data['Account']
203
+ true
204
+ else
205
+ @aws_error = output.strip
206
+ false
207
+ end
208
+ end
209
+
210
+ def aws_account_id
211
+ @aws_account_id
188
212
  end
189
213
 
190
214
  def bucket_exists?(bucket)
191
- system('aws', 's3api', 'head-bucket', '--bucket', bucket, err: File::NULL)
215
+ output, status = Open3.capture2e('aws', 's3api', 'head-bucket', '--bucket', bucket)
216
+ return true if status.success?
217
+
218
+ # 403 = bucket exists but owned by someone else; 404 = doesn't exist
219
+ if output.include?('403') || output.include?('Forbidden')
220
+ puts "\n✗ Bucket '#{bucket}' exists but is owned by a different AWS account."
221
+ puts ' S3 bucket names are globally unique. Choose a different name with --bucket.'
222
+ exit 1
223
+ end
224
+
225
+ false
192
226
  end
193
227
 
194
228
  def create_bucket(bucket)
data/lib/belt/cli.rb CHANGED
@@ -11,6 +11,8 @@ require_relative 'cli/frontend_deploy_command'
11
11
  require_relative 'cli/views_command'
12
12
  require_relative 'cli/setup_command'
13
13
  require_relative 'cli/terraform_command'
14
+ require_relative 'cli/deploy_command'
15
+ require_relative 'cli/server_command'
14
16
  require_relative 'cli/routes_command'
15
17
  require_relative 'cli/tasks_command'
16
18
  require_relative 'cli/console_command'
@@ -24,15 +26,8 @@ module Belt
24
26
  %w[console c] => Belt::CLI::ConsoleCommand,
25
27
  %w[tasks --tasks -T] => Belt::CLI::TasksCommand,
26
28
  'setup' => Belt::CLI::SetupCommand,
27
- 'deploy' => lambda { |args|
28
- subcommand = args.shift
29
- if subcommand == 'frontend'
30
- Belt::CLI::FrontendDeployCommand.run(args)
31
- else
32
- puts 'Usage: belt deploy frontend <environment>'
33
- exit 1
34
- end
35
- },
29
+ 'deploy' => Belt::CLI::DeployCommand,
30
+ %w[server s] => Belt::CLI::ServerCommand,
36
31
  %w[version --version -v] => ->(_args) { puts "Belt #{Belt::VERSION}" }
37
32
  }.freeze
38
33
 
@@ -80,6 +75,10 @@ module Belt
80
75
  generate frontend <react|vue|svelte> Scaffold a frontend app
81
76
  generate views <resource> [fields...] Generate React pages for REST actions
82
77
  generate environment <name> Create a new environment
78
+ server Start local dev server (frontend)
79
+ s Alias for server
80
+ deploy [environment] Deploy to AWS (init → plan → apply)
81
+ deploy frontend <env> Build and deploy frontend to AWS
83
82
  routes [-g PATTERN] [-f json] Show route definitions
84
83
  console Start an interactive console (IRB)
85
84
  c Alias for console
@@ -88,7 +87,6 @@ module Belt
88
87
  setup state Create/select S3 state bucket
89
88
  setup tables <env> Generate DynamoDB tables from schema
90
89
  setup frontend <env> Generate S3 + CloudFront infrastructure
91
- deploy frontend <env> Build and deploy frontend to AWS
92
90
  init [environment] <env> terraform init for environment
93
91
  plan [environment] <env> terraform plan for environment
94
92
  apply [environment] <env> terraform apply for environment
@@ -110,8 +108,11 @@ module Belt
110
108
  belt new blog --frontend react
111
109
  belt generate resource post title:string content:text status:string
112
110
  belt generate frontend react
113
- belt setup frontend wups
111
+ belt server # Start local frontend server
112
+ belt deploy # Deploy dev to AWS
113
+ belt deploy prod --auto # Deploy prod without confirmation
114
114
  belt deploy frontend wups
115
+ belt setup frontend wups
115
116
  belt apply wups
116
117
  belt tasks # list all rake tasks
117
118
  belt lambda:build_layer # run a rake task directly
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+
5
+ module Belt
6
+ # Gem-embedded welcome controller that serves the default "it works!" page.
7
+ # Resolved via the ActionRouter's Belt:: namespace fallback when no app-defined
8
+ # WelcomeController exists. Gets replaced once the user defines their own root route.
9
+ class WelcomeController < BeltController::Base
10
+ skip_before_action :authenticate!
11
+
12
+ ASSETS_DIR = File.expand_path('../assets', __dir__)
13
+
14
+ def show
15
+ @title = ENV.fetch('WELCOME_TITLE', 'Welcome to Belt')
16
+ @subtitle = ENV.fetch('WELCOME_SUBTITLE', 'API Gateway → Lambda → DynamoDB — all connected.')
17
+ @css = welcome_css
18
+ @background_image = background_image_base64
19
+ @dynamodb_connected = dynamodb_connected?
20
+
21
+ render
22
+ end
23
+
24
+ private
25
+
26
+ def dynamodb_connected?
27
+ return false unless defined?(Aws::DynamoDB::Client)
28
+
29
+ client = Aws::DynamoDB::Client.new
30
+ client.list_tables(limit: 1)
31
+ true
32
+ rescue StandardError
33
+ false
34
+ end
35
+
36
+ def welcome_css
37
+ @welcome_css_content ||= File.read(File.join(ASSETS_DIR, 'welcome.css'))
38
+ end
39
+
40
+ def background_image_base64
41
+ @background_image_content ||= Base64.strict_encode64(
42
+ File.binread(File.join(ASSETS_DIR, 'belt-default.jpg'))
43
+ )
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'cgi'
5
+
6
+ module Belt
7
+ # Provides Rails-like ERB rendering for BeltController.
8
+ #
9
+ # Convention: templates live at `views/<controller_name>/<action>.html.erb`
10
+ # relative to the controller's gem or app directory.
11
+ #
12
+ # Usage:
13
+ # class WelcomeController < BeltController::Base
14
+ # def show
15
+ # @title = "Hello"
16
+ # render # auto-resolves to views/welcome/show.html.erb
17
+ # end
18
+ # end
19
+ #
20
+ # Or explicit:
21
+ # render template: "welcome/show"
22
+ # render inline: "<h1><%= @title %></h1>"
23
+ #
24
+ module Rendering
25
+ def self.included(base)
26
+ base.extend(ClassMethods)
27
+ end
28
+
29
+ module ClassMethods
30
+ # Override in subclasses to set a custom view path.
31
+ # Defaults to `views/` relative to the file that defines the controller.
32
+ def view_paths
33
+ @view_paths ||= []
34
+ end
35
+
36
+ def prepend_view_path(path)
37
+ view_paths.unshift(path)
38
+ end
39
+
40
+ def append_view_path(path)
41
+ view_paths << path
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ # Render an ERB template and return an html_response.
48
+ #
49
+ # Options:
50
+ # render — auto-resolve from controller/action
51
+ # render template: "ctrl/action" — explicit template path (no extension)
52
+ # render inline: "<%= @x %>" — render a string as ERB
53
+ # render status: 201 — custom status code
54
+ # render layout: false — skip layout (default: no layout)
55
+ #
56
+ def render(options = {})
57
+ status = options.fetch(:status, 200)
58
+
59
+ html = if options.key?(:inline)
60
+ render_erb_string(options[:inline])
61
+ else
62
+ template_path = resolve_template_path(options[:template])
63
+ render_erb_file(template_path)
64
+ end
65
+
66
+ html_response(html, status)
67
+ end
68
+
69
+ def render_erb_string(source)
70
+ erb = ERB.new(source, trim_mode: '-')
71
+ erb.result(binding)
72
+ end
73
+
74
+ def render_erb_file(path)
75
+ raise Belt::TemplateNotFound, "Template not found: #{path}" unless File.exist?(path)
76
+
77
+ source = File.read(path)
78
+ erb = ERB.new(source, trim_mode: '-')
79
+ erb.filename = path
80
+ erb.result(binding)
81
+ end
82
+
83
+ # HTML-escape helper, available in templates as `h(value)`
84
+ def h(text)
85
+ CGI.escapeHTML(text.to_s)
86
+ end
87
+
88
+ def resolve_template_path(explicit_template = nil)
89
+ template_name = explicit_template || default_template_name
90
+ search_paths = build_view_search_paths
91
+
92
+ search_paths.each do |base|
93
+ full_path = File.join(base, "#{template_name}.html.erb")
94
+ return full_path if File.exist?(full_path)
95
+ end
96
+
97
+ # If nothing found, return the first candidate so the error message is useful
98
+ File.join(search_paths.first || '.', "#{template_name}.html.erb")
99
+ end
100
+
101
+ def default_template_name
102
+ # PostsController → posts, Belt::WelcomeController → welcome
103
+ controller = self.class.name.split('::').last
104
+ .sub(/Controller$/, '')
105
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
106
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
107
+ .downcase
108
+ "#{controller}/#{action_name}"
109
+ end
110
+
111
+ def build_view_search_paths
112
+ paths = []
113
+
114
+ # 1. Class-level view paths (set by the controller or gem)
115
+ paths.concat(self.class.view_paths) if self.class.respond_to?(:view_paths)
116
+
117
+ # 2. Walk up the class hierarchy for inherited view paths
118
+ klass = self.class.superclass
119
+ while klass.respond_to?(:view_paths)
120
+ paths.concat(klass.view_paths)
121
+ klass = klass.superclass
122
+ end
123
+
124
+ # 3. App-level views (convention: lambda/views/ from Belt.root)
125
+ if defined?(Belt.root) && Belt.root
126
+ paths << File.join(Belt.root, 'lambda', 'views')
127
+ paths << File.join(Belt.root, 'views')
128
+ end
129
+
130
+ # 4. Gem-level fallback (views/ relative to this file — belt gem's own views)
131
+ paths << File.expand_path('views', __dir__)
132
+
133
+ paths.uniq
134
+ end
135
+ end
136
+ end
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.1.4'
4
+ VERSION = '0.1.6'
5
5
  end
@@ -0,0 +1,51 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= h(@title) %></title>
7
+ <style><%= @css %></style>
8
+ </head>
9
+ <body>
10
+ <div class="hero">
11
+ <img src="data:image/jpeg;base64,<%= @background_image %>" alt="Belt" class="hero-bg" />
12
+ <div class="overlay">
13
+ <h1><%= h(@title) %></h1>
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>
27
+ </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>
33
+ </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
+ </div>
49
+ </div>
50
+ </body>
51
+ </html>
data/lib/belt.rb CHANGED
@@ -11,6 +11,7 @@ module Belt
11
11
  class AuthenticationError < StandardError; end
12
12
  class RecordNotFound < StandardError; end
13
13
  class ActionNotFound < StandardError; end
14
+ class TemplateNotFound < StandardError; end
14
15
 
15
16
  @controller_paths = []
16
17
 
@@ -55,3 +56,4 @@ module Belt
55
56
  end
56
57
 
57
58
  require_relative 'belt_controller/base'
59
+ require_relative 'belt/controllers/welcome_controller'