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.
data/lib/belt.rb CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  require_relative 'belt/version'
4
4
  require_relative 'belt/root'
5
+ require_relative 'belt/configuration'
6
+ require_relative 'belt/http_status'
5
7
  require_relative 'belt/parameters'
6
8
  require_relative 'belt/observability'
7
9
  require_relative 'belt/lambda_handler'
@@ -18,6 +20,20 @@ module Belt
18
20
  class << self
19
21
  attr_reader :controller_paths
20
22
 
23
+ # Runtime configuration (lambda/config/environment.rb). Separate from the
24
+ # CLI sandboxed DSL in infrastructure/<env>/belt.rb.
25
+ def configuration
26
+ @configuration ||= Configuration.new
27
+ end
28
+
29
+ def configure
30
+ yield configuration
31
+ end
32
+
33
+ def reset_configuration!
34
+ @configuration = Configuration.new
35
+ end
36
+
21
37
  # Auto-discover lambda/controllers dirs in all loaded gems
22
38
  def gem_controller_paths
23
39
  @gem_controller_paths ||= discover_gem_paths('lambda/controllers')
@@ -7,11 +7,13 @@ require_relative '../belt/helpers/response'
7
7
  require_relative '../belt/helpers/error_logging'
8
8
  require_relative '../belt/helpers/cors_origin'
9
9
  require_relative '../belt/rendering'
10
+ require_relative 'implicit_response'
10
11
 
11
12
  module BeltController
12
13
  class Base
13
14
  include Belt::Helpers::Response
14
15
  include Belt::Rendering
16
+ include ImplicitResponse
15
17
 
16
18
  attr_reader :event, :body
17
19
 
@@ -48,6 +50,25 @@ module BeltController
48
50
  skipped_before_actions << { method: method_name, only: only&.map(&:to_sym), except: except&.map(&:to_sym) }
49
51
  end
50
52
 
53
+ # Implicit response format when the action does not call success_response /
54
+ # error_response / html_response / render / head.
55
+ # Inherits up the controller chain; falls back to Belt.configuration.default_format.
56
+ def default_format
57
+ return @default_format if defined?(@default_format)
58
+ return superclass.default_format if superclass.respond_to?(:default_format)
59
+
60
+ Belt.configuration.default_format
61
+ end
62
+
63
+ def default_format=(value)
64
+ format = value.to_sym
65
+ unless Belt::Configuration::VALID_FORMATS.include?(format)
66
+ raise ArgumentError, "default_format must be :json or :html (got #{value.inspect})"
67
+ end
68
+
69
+ @default_format = format
70
+ end
71
+
51
72
  def all_before_actions
52
73
  if superclass.respond_to?(:all_before_actions)
53
74
  superclass.all_before_actions + before_actions
@@ -115,9 +136,11 @@ module BeltController
115
136
  end
116
137
 
117
138
  run_before_actions(action_sym)
139
+ # Snapshot ivars so we only auto-serialize assigns set by the action (Rails-style)
140
+ @__assigns_before = instance_variables
118
141
  result = send(action_sym)
119
142
  run_after_actions(action_sym)
120
- result
143
+ finalize_response(result)
121
144
  rescue StandardError => e
122
145
  handle_exception(e)
123
146
  end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BeltController
4
+ # Rails-like implicit JSON/HTML responses from action assigns / return values.
5
+ # Included by BeltController::Base so the base class stays under Metrics/ClassLength.
6
+ module ImplicitResponse
7
+ # Never auto-serialize these into the JSON body (framework internals).
8
+ FRAMEWORK_IVARS = %i[
9
+ @event @raw_body @params @current_action @current_user_id @user_groups
10
+ @logger @__assigns_before @__response_status
11
+ ].freeze
12
+
13
+ private
14
+
15
+ # Turn the action return value (or instance-variable assigns) into an API Gateway response.
16
+ #
17
+ # Rails-like behavior for API-first controllers (default_format :json):
18
+ # def index
19
+ # @posts = Post.all # → success_response({ posts: [...] })
20
+ # end
21
+ #
22
+ # HTML controllers (default_format :html):
23
+ # def show
24
+ # @post = Post.find(...) # → render template views/<ctrl>/show.html.erb
25
+ # end
26
+ #
27
+ # Explicit responses still win:
28
+ # success_response(...), error_response(...), html_response(...), render, head
29
+ #
30
+ # Non-200 with implicit body:
31
+ # def create
32
+ # @post = Post.create!(...)
33
+ # response_status :created
34
+ # end
35
+ def finalize_response(result)
36
+ return result if lambda_response?(result)
37
+
38
+ status = @__response_status || 200
39
+
40
+ return render(status: status) if self.class.default_format == :html
41
+
42
+ # Prefer assigns when present. Ruby assignment returns the RHS, so
43
+ # `@posts = Post.all` would otherwise look like an explicit return value.
44
+ # (Rails ignores the action return value entirely; we keep a thin fallback
45
+ # for explicit non-assign returns.)
46
+ assigns = collect_assigns
47
+ return success_response(assigns, status) if assigns.any?
48
+
49
+ return success_response({}, status) if result.nil?
50
+
51
+ success_response(serialize_for_response(result), status)
52
+ end
53
+
54
+ def lambda_response?(result)
55
+ result.is_a?(Hash) && (result.key?(:statusCode) || result.key?('statusCode'))
56
+ end
57
+
58
+ # Instance variables set during the action (excluding framework internals).
59
+ # Keys are the ivar names without @ — so @posts becomes { posts: ... }.
60
+ def collect_assigns
61
+ before = @__assigns_before || []
62
+ (instance_variables - before - FRAMEWORK_IVARS).each_with_object({}) do |ivar, hash|
63
+ key = ivar.to_s.delete_prefix('@')
64
+ next if key.start_with?('_')
65
+
66
+ hash[key.to_sym] = serialize_for_response(instance_variable_get(ivar))
67
+ end
68
+ end
69
+
70
+ # Deep-serialize models/relations into JSON-friendly hashes/arrays.
71
+ # ActiveItem::Relation (and any Enumerable of models) maps via to_h on each record.
72
+ def serialize_for_response(value)
73
+ case value
74
+ when nil, String, Numeric, TrueClass, FalseClass
75
+ value
76
+ when Symbol
77
+ value.to_s
78
+ when Hash
79
+ value.each_with_object({}) do |(k, v), h|
80
+ h[k.is_a?(Symbol) ? k : k.to_s] = serialize_for_response(v)
81
+ end
82
+ when Array
83
+ value.map { |v| serialize_for_response(v) }
84
+ else
85
+ serialize_object(value)
86
+ end
87
+ end
88
+
89
+ def serialize_object(value)
90
+ # Relations are Enumerable — map each record (don't call Enumerable#to_h)
91
+ if defined?(ActiveItem::Relation) && value.is_a?(ActiveItem::Relation)
92
+ value.map { |record| serialize_for_response(record) }
93
+ elsif value.respond_to?(:to_h) && !value.is_a?(Enumerable)
94
+ serialize_for_response(value.to_h)
95
+ elsif value.is_a?(Enumerable)
96
+ value.map { |item| serialize_for_response(item) }
97
+ elsif value.respond_to?(:to_h)
98
+ serialize_for_response(value.to_h)
99
+ else
100
+ value
101
+ end
102
+ end
103
+ end
104
+ end
@@ -1,5 +1,6 @@
1
1
  terraform {
2
2
  backend "s3" {
3
+ # Resolved to belt-terraform-state-<account-id> by `belt setup state` / `belt new`
3
4
  bucket = "belt-terraform-state"
4
5
  key = "<%= s3_safe_name(@app_name) %>/<%= @env_name %>/terraform.tfstate"
5
6
  region = "us-east-1"
@@ -1,10 +1,10 @@
1
1
  :root {
2
- font-family: system-ui, -apple-system, sans-serif;
2
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
3
3
  line-height: 1.5;
4
- color: #213547;
5
- background-color: #ffffff;
4
+ color: #2d3748;
5
+ background-color: #f8f9fa;
6
6
  }
7
7
 
8
8
  * { box-sizing: border-box; margin: 0; padding: 0; }
9
9
 
10
- body { min-height: 100vh; }
10
+ body { min-height: 100vh; background: #f8f9fa; }
@@ -3,15 +3,32 @@ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'
3
3
  export async function apiClient(path, options = {}) {
4
4
  const { method = 'GET', body, headers = {} } = options
5
5
 
6
+ // Only set Content-Type when sending a body. application/json on a GET forces a
7
+ // CORS preflight; Accept alone is safelisted and stays a simple request.
6
8
  const config = {
7
9
  method,
8
- headers: { 'Content-Type': 'application/json', ...headers }
10
+ headers: {
11
+ Accept: 'application/json',
12
+ ...(body !== undefined && body !== null ? { 'Content-Type': 'application/json' } : {}),
13
+ ...headers
14
+ }
9
15
  }
10
16
 
11
- if (body) config.body = JSON.stringify(body)
17
+ if (body !== undefined && body !== null) config.body = JSON.stringify(body)
12
18
 
13
19
  const response = await fetch(`${API_URL}${path}`, config)
14
- const data = await response.json()
20
+
21
+ let data
22
+ const text = await response.text()
23
+ try {
24
+ data = text ? JSON.parse(text) : {}
25
+ } catch {
26
+ throw new Error(
27
+ response.ok
28
+ ? 'API returned non-JSON (is VITE_API_URL pointing at the API Gateway?)'
29
+ : `Request failed: ${response.status}`
30
+ )
31
+ }
15
32
 
16
33
  if (!response.ok) throw new Error(data.error || `Request failed: ${response.status}`)
17
34
 
@@ -1,9 +1,288 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { apiClient } from '../lib/apiClient'
3
+
4
+ // Mirrors lib/belt/assets/welcome.css so SPA and Lambda HTML welcome match.
5
+ // All content lives in the hero overlay — no scroll under the poster.
6
+ const welcomeStyles = `
7
+ * { margin: 0; padding: 0; box-sizing: border-box; }
8
+ html, body, #root { height: 100%; }
9
+ .belt-welcome {
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11
+ background: #0b1220;
12
+ color: #2d3748;
13
+ min-height: 100vh;
14
+ height: 100%;
15
+ overflow: hidden;
16
+ }
17
+ .belt-welcome .hero {
18
+ position: relative;
19
+ width: 100%;
20
+ height: 100vh;
21
+ overflow: hidden;
22
+ }
23
+ .belt-welcome .hero-bg {
24
+ width: 100%;
25
+ height: 100%;
26
+ object-fit: cover;
27
+ /* Pin the logo / sky — crop hills at the bottom if the viewport is short */
28
+ object-position: top center;
29
+ display: block;
30
+ }
31
+ .belt-welcome .overlay {
32
+ position: absolute;
33
+ top: 50%;
34
+ left: 50%;
35
+ transform: translate(-50%, -50%);
36
+ padding: 1.35rem 1.6rem 1.25rem;
37
+ background: rgba(255, 255, 255, 0.55);
38
+ backdrop-filter: blur(8px);
39
+ -webkit-backdrop-filter: blur(8px);
40
+ border-radius: 14px;
41
+ text-align: center;
42
+ max-width: 560px;
43
+ width: min(92vw, 560px);
44
+ max-height: min(92vh, 620px);
45
+ overflow: auto;
46
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
47
+ }
48
+ .belt-welcome .overlay h1 {
49
+ font-size: 1.85rem;
50
+ font-weight: 700;
51
+ color: #2d3748;
52
+ margin-bottom: 0.3rem;
53
+ line-height: 1.2;
54
+ }
55
+ .belt-welcome .overlay .subtitle {
56
+ font-size: 0.95rem;
57
+ color: #4a5568;
58
+ line-height: 1.4;
59
+ }
60
+ .belt-welcome .stack-check {
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: center;
64
+ gap: 0.4rem;
65
+ margin: 1rem 0 0.85rem;
66
+ flex-wrap: wrap;
67
+ }
68
+ .belt-welcome .check-item {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 0.35rem;
72
+ padding: 0.35rem 0.7rem;
73
+ border-radius: 8px;
74
+ font-weight: 500;
75
+ font-size: 0.88rem;
76
+ }
77
+ .belt-welcome .check-pass {
78
+ background: rgba(39, 174, 96, 0.12);
79
+ border: 1px solid rgba(39, 174, 96, 0.35);
80
+ color: #1e8449;
81
+ }
82
+ .belt-welcome .check-neutral {
83
+ background: rgba(113, 128, 150, 0.1);
84
+ border: 1px solid rgba(113, 128, 150, 0.28);
85
+ color: #4a5568;
86
+ }
87
+ .belt-welcome .icon { font-size: 1rem; }
88
+ .belt-welcome .arrow { color: #718096; font-size: 1.05rem; }
89
+ .belt-welcome .next-steps {
90
+ text-align: left;
91
+ border-top: 1px solid rgba(45, 55, 72, 0.12);
92
+ padding-top: 0.75rem;
93
+ margin-top: 0.15rem;
94
+ }
95
+ .belt-welcome .next-steps h2 {
96
+ font-size: 0.92rem;
97
+ margin-bottom: 0.45rem;
98
+ color: #2d3748;
99
+ text-align: left;
100
+ }
101
+ .belt-welcome .next-steps ol {
102
+ padding-left: 1.2rem;
103
+ margin: 0;
104
+ }
105
+ .belt-welcome .next-steps li {
106
+ margin-bottom: 0.4rem;
107
+ color: #4a5568;
108
+ line-height: 1.4;
109
+ font-size: 0.88rem;
110
+ }
111
+ .belt-welcome .next-steps li:last-child { margin-bottom: 0; }
112
+ .belt-welcome .next-steps code {
113
+ background: rgba(99, 45, 145, 0.1);
114
+ padding: 0.12rem 0.35rem;
115
+ border-radius: 4px;
116
+ color: #632d91;
117
+ font-size: 0.78rem;
118
+ word-break: break-word;
119
+ }
120
+ .belt-welcome .footer-note {
121
+ margin-top: 0.75rem;
122
+ padding-top: 0.65rem;
123
+ border-top: 1px solid rgba(45, 55, 72, 0.1);
124
+ font-size: 0.78rem;
125
+ color: #718096;
126
+ text-align: center;
127
+ }
128
+ .belt-welcome .footer-note code {
129
+ background: rgba(45, 55, 72, 0.06);
130
+ padding: 0.08rem 0.3rem;
131
+ border-radius: 3px;
132
+ color: #4a5568;
133
+ }
134
+ .belt-welcome .status-main {
135
+ padding: 3rem 2rem;
136
+ text-align: center;
137
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
138
+ color: #4a5568;
139
+ min-height: 100vh;
140
+ display: flex;
141
+ flex-direction: column;
142
+ align-items: center;
143
+ justify-content: center;
144
+ }
145
+ .belt-welcome .status-main h1 {
146
+ font-size: 1.75rem;
147
+ color: #2d3748;
148
+ margin-bottom: 0.75rem;
149
+ }
150
+ .belt-welcome .error { color: #b91c1c; margin-top: 0.75rem; }
151
+ .belt-welcome .muted { color: #6b7280; margin-top: 0.5rem; font-size: 0.95rem; }
152
+ @media (max-height: 640px) {
153
+ .belt-welcome .overlay { padding: 1rem 1.15rem; }
154
+ .belt-welcome .overlay h1 { font-size: 1.5rem; }
155
+ .belt-welcome .overlay .subtitle { font-size: 0.88rem; }
156
+ .belt-welcome .stack-check { margin: 0.7rem 0 0.55rem; }
157
+ .belt-welcome .check-item { padding: 0.28rem 0.55rem; font-size: 0.8rem; }
158
+ .belt-welcome .next-steps li { font-size: 0.82rem; }
159
+ }
160
+ `
161
+
162
+ function StackItem({ ok, label }) {
163
+ // DynamoDB false is expected (no tables yet) — neutral, not a warning.
164
+ const className = ok ? 'check-item check-pass' : 'check-item check-neutral'
165
+ return (
166
+ <div className={className}>
167
+ <span className="icon" aria-hidden="true">{ok ? '✓' : '○'}</span>
168
+ <span>{label}</span>
169
+ </div>
170
+ )
171
+ }
172
+
173
+ // Turn "Label: belt some command" into mixed text + <code> when possible.
174
+ function StepLine({ step }) {
175
+ const match = typeof step === 'string' ? step.match(/^(.*?):\s+(belt\s.+)$/) : null
176
+ if (match) {
177
+ return (
178
+ <>
179
+ {match[1]}: <code>{match[2]}</code>
180
+ </>
181
+ )
182
+ }
183
+ return step
184
+ }
185
+
1
186
  function Home() {
187
+ const [welcome, setWelcome] = useState(null)
188
+ const [error, setError] = useState(null)
189
+ const [loading, setLoading] = useState(true)
190
+
191
+ useEffect(() => {
192
+ let cancelled = false
193
+
194
+ apiClient('/')
195
+ .then((data) => {
196
+ if (!cancelled) {
197
+ setWelcome(data)
198
+ setLoading(false)
199
+ }
200
+ })
201
+ .catch((err) => {
202
+ if (!cancelled) {
203
+ setError(err.message || 'Failed to reach API')
204
+ setLoading(false)
205
+ }
206
+ })
207
+
208
+ return () => { cancelled = true }
209
+ }, [])
210
+
211
+ if (loading) {
212
+ return (
213
+ <div className="belt-welcome">
214
+ <style>{welcomeStyles}</style>
215
+ <main className="status-main">
216
+ <p className="muted">Connecting to API…</p>
217
+ </main>
218
+ </div>
219
+ )
220
+ }
221
+
222
+ if (error) {
223
+ return (
224
+ <div className="belt-welcome">
225
+ <style>{welcomeStyles}</style>
226
+ <main className="status-main">
227
+ <h1><%= @module_name %></h1>
228
+ <p className="error">Could not load welcome data from the API.</p>
229
+ <p className="muted">{error}</p>
230
+ <p className="muted" style={{ marginTop: '1rem' }}>
231
+ Check <code>VITE_API_URL</code> (run <code>belt frontend env dev</code>) and that CORS allows this origin.
232
+ </p>
233
+ </main>
234
+ </div>
235
+ )
236
+ }
237
+
238
+ const stack = welcome.stack || {}
239
+ const dynamoOk = !!stack.dynamodb
240
+ const title = welcome.title || '<%= @module_name %>'
241
+ const subtitle = welcome.subtitle || 'Your Belt app is running.'
242
+
2
243
  return (
3
- <main style={{ padding: '2rem', maxWidth: '800px', margin: '0 auto' }}>
4
- <h1><%= @module_name %></h1>
5
- <p>Your Belt app is running. Edit <code>src/pages/Home.jsx</code> to get started.</p>
6
- </main>
244
+ <div className="belt-welcome">
245
+ <style>{welcomeStyles}</style>
246
+
247
+ <div className="hero">
248
+ {welcome.background_image ? (
249
+ <img
250
+ src={welcome.background_image}
251
+ alt="Belt"
252
+ className="hero-bg"
253
+ />
254
+ ) : (
255
+ <div className="hero-bg" style={{ minHeight: '100vh', background: '#1a202c' }} />
256
+ )}
257
+ <div className="overlay">
258
+ <h1>{title}</h1>
259
+ <p className="subtitle">{subtitle}</p>
260
+
261
+ <div className="stack-check">
262
+ <StackItem ok={!!stack.api_gateway} label="API Gateway" />
263
+ <span className="arrow">→</span>
264
+ <StackItem ok={!!stack.lambda} label="Lambda" />
265
+ <span className="arrow">→</span>
266
+ <StackItem ok={dynamoOk} label="DynamoDB" />
267
+ </div>
268
+
269
+ {Array.isArray(welcome.next_steps) && welcome.next_steps.length > 0 && (
270
+ <div className="next-steps">
271
+ <h2>Next Steps</h2>
272
+ <ol>
273
+ {welcome.next_steps.map((step, i) => (
274
+ <li key={i}><StepLine step={step} /></li>
275
+ ))}
276
+ </ol>
277
+ </div>
278
+ )}
279
+
280
+ <p className="footer-note">
281
+ Edit <code>src/pages/Home.jsx</code> to replace this page.
282
+ </p>
283
+ </div>
284
+ </div>
285
+ </div>
7
286
  )
8
287
  }
9
288
 
@@ -21,17 +21,27 @@ terraform {
21
21
  resource "conveyor_belt" "main" {
22
22
  provider = conveyor-belt
23
23
 
24
- source = "${path.module}/../../config/routes.tf.rb"
24
+ # path.module is infrastructure/modules/app — climb three levels to project root
25
+ source = "${path.module}/../../../config/routes.tf.rb"
25
26
  app_name = var.app_name
26
27
  lambda_source_dir = "${path.module}/../../../lambda"
27
28
  lambda_shared_dirs = ["controllers", "helpers", "lib", "models", "views"]
28
- frontend_urls = var.frontend_urls
29
+ <% if @frontend -%>
30
+ # CloudFront first so SPA→API CORS works out of the box (tutorial footgun fixed).
31
+ # localhost stays via env frontend_urls for local `belt server` dev.
32
+ frontend_urls = concat(
33
+ ["https://${aws_cloudfront_distribution.frontend.domain_name}"],
34
+ var.frontend_urls
35
+ )
36
+ <% else -%>
37
+ frontend_urls = var.frontend_urls
38
+ <% end -%>
29
39
  friendly_errors = var.environment != "prod"
30
40
 
31
41
  custom_domain_name = local.dns_enabled ? local.api_domain : ""
32
42
 
33
43
  # Per-lambda configuration from config/lambda/*.yml (database.yml style)
34
- lambda_config_dir = "${path.module}/../../config/lambda"
44
+ lambda_config_dir = "${path.module}/../../../config/lambda"
35
45
 
36
46
  # Dynamic value references — YAML env_vars use ref(name) to pull these in
37
47
  lambda_env_refs = var.lambda_env_refs
@@ -1,6 +1,7 @@
1
1
  Belt.application.routes.draw do
2
2
  namespace :api do
3
- get "/", action: :show, controller: :welcome
3
+ # Public stack-check / welcome (HTML for browsers, JSON for the SPA shell)
4
+ get "/", action: :show, controller: :welcome, auth: :none
4
5
  # resources :posts
5
6
  end
6
7
  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.9
4
+ version: 0.2.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stowzilla
@@ -76,6 +76,7 @@ files:
76
76
  - lib/belt/cli/console_command.rb
77
77
  - lib/belt/cli/deploy_command.rb
78
78
  - lib/belt/cli/destroy_command.rb
79
+ - lib/belt/cli/doctor_command.rb
79
80
  - lib/belt/cli/env_resolver.rb
80
81
  - lib/belt/cli/environment_command.rb
81
82
  - lib/belt/cli/environment_config.rb
@@ -88,6 +89,7 @@ files:
88
89
  - lib/belt/cli/generator_registry.rb
89
90
  - lib/belt/cli/lambda_config_command.rb
90
91
  - lib/belt/cli/new_command.rb
92
+ - lib/belt/cli/path_gem_materializer.rb
91
93
  - lib/belt/cli/routes_command.rb
92
94
  - lib/belt/cli/routes_command/route_inference.rb
93
95
  - lib/belt/cli/routes_command/schema_loader.rb
@@ -97,10 +99,12 @@ files:
97
99
  - lib/belt/cli/tasks_command.rb
98
100
  - lib/belt/cli/terraform_command.rb
99
101
  - lib/belt/cli/views_command.rb
102
+ - lib/belt/configuration.rb
100
103
  - lib/belt/controllers/welcome_controller.rb
101
104
  - lib/belt/helpers/cors_origin.rb
102
105
  - lib/belt/helpers/error_logging.rb
103
106
  - lib/belt/helpers/response.rb
107
+ - lib/belt/http_status.rb
104
108
  - lib/belt/inflector.rb
105
109
  - lib/belt/lambda_handler.rb
106
110
  - lib/belt/observability.rb
@@ -112,6 +116,7 @@ files:
112
116
  - lib/belt/version.rb
113
117
  - lib/belt/views/welcome/show.html.erb
114
118
  - lib/belt_controller/base.rb
119
+ - lib/belt_controller/implicit_response.rb
115
120
  - lib/templates/environment/backend.tf.erb
116
121
  - lib/templates/environment/main.tf.erb
117
122
  - lib/templates/environment/outputs.tf.erb
@@ -160,6 +165,19 @@ metadata:
160
165
  source_code_uri: https://github.com/stowzilla/belt
161
166
  changelog_uri: https://github.com/stowzilla/belt/blob/master/CHANGELOG.md
162
167
  rubygems_mfa_required: 'true'
168
+ post_install_message: |2+
169
+
170
+ ╭────────────────────────────────────────────╮
171
+ │ Belt installed successfully! │
172
+ │ │
173
+ │ Run `belt doctor` to verify your setup: │
174
+ │ • AWS CLI │
175
+ │ • Terraform │
176
+ │ • AWS credentials & authentication │
177
+ │ │
178
+ │ Then: belt new <app_name> │
179
+ ╰────────────────────────────────────────────╯
180
+
163
181
  rdoc_options: []
164
182
  require_paths:
165
183
  - lib
@@ -178,3 +196,4 @@ rubygems_version: 3.6.9
178
196
  specification_version: 4
179
197
  summary: Belt - a utility toolkit for Ruby applications
180
198
  test_files: []
199
+ ...