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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 37cf283dc30b43f875f8600eb4b00e46b5782f374d3be306d0c5adaec7e9024a
4
- data.tar.gz: 1153f552cef87426f91afcf28647065017b77a09bdd9347241366ed9cc455b76
3
+ metadata.gz: 4a550043d0d16c901e656de4a9d89a0f473d77778121e2b27fb8791f3edf74d4
4
+ data.tar.gz: f9404b6e69d725650ee612af06b96917886d2856fca14955bb99cb9555811dcd
5
5
  SHA512:
6
- metadata.gz: 4cafc0f29e0148e74950e9fe0898e74632f2563980f60ffece7bfbb58ec7187ec30f928a0958dff9d9832fe79b21e8a87e512f26c4cddfa58c1f243776cea5eb
7
- data.tar.gz: 7850fc8e4c5921646c25f77f67fdfcae663e148ec597b514b095b637733b0e55f34182e8c64ea52f2294ed687b3a24f7b6a56462b30df07811a01ffae23da952
6
+ metadata.gz: e719bed32a829bf952c22885d63a1415ea292c3b1770a7f7bdb398ef874a5182a1b8208654e2672d6d7fcccbb4b2e9f24a82186708ed3982d9ca7ec94d9bedbf
7
+ data.tar.gz: 32fa436e2a97b5d3597af8d2afcfb8657c5de91f12a01b551d8368be97a80fdfd544e5b6b711a930ff741c1e8228d393088893700f13c719b454b59e01c3d6a0
data/CHANGELOG.md CHANGED
@@ -1,5 +1,92 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.10
4
+
5
+ ### Quiet `belt new` (with optional verbose)
6
+
7
+ `belt new` prints a short phase summary by default (skeleton, environments, frontend,
8
+ bundle, state bucket) instead of every path plus AWS/npm noise.
9
+
10
+ Use `-v` / `--verbose` for Rails-style per-file `create` lines when you want the
11
+ inventory. Nested generators skip their own next-steps banners; the final success
12
+ block still owns next steps. State-bucket setup stays non-interactive either way.
13
+
14
+ ### State bucket naming (global uniqueness)
15
+
16
+ S3 bucket names are a **global** namespace across all AWS accounts. The previous default
17
+ `belt-terraform-state` only works for the first account that creates it — everyone else
18
+ hits "owned by a different AWS account".
19
+
20
+ **Fix:** shared bucket is still one-per-account (all apps share it), but the name is now:
21
+
22
+ ```
23
+ belt-terraform-state-<account_id>
24
+ ```
25
+
26
+ - Still one bucket for all belt apps in an account (state key: `<app>/<env>/terraform.tfstate`)
27
+ - `--bucket` still overrides when you want a custom name
28
+ - `belt setup state` rewrites `backend.tf` with the resolved name
29
+ - Existing installs that already own `belt-terraform-state` can keep using `--bucket belt-terraform-state`
30
+
31
+ ### Implicit responses (Rails-style assigns)
32
+
33
+ Controllers can set instance variables and skip the explicit `success_response` call:
34
+
35
+ ```ruby
36
+ def index
37
+ @posts = Post.all
38
+ end
39
+
40
+ def show
41
+ @post = Post.find(params[:id])
42
+ end
43
+ ```
44
+
45
+ Belt auto-builds `success_response({ posts: [...] })` / `{ post: {...} }` from assigns set
46
+ during the action. Serialization:
47
+
48
+ - Models → `to_h` (ActiveItem already defines this)
49
+ - `ActiveItem::Relation` / Enumerable → map each record via `to_h`
50
+ - Nested hashes/arrays recurse
51
+
52
+ Explicit `success_response` / `error_response` / `html_response` / `render` / `head` still win
53
+ when returned. Rescue handlers still use `error_response`.
54
+
55
+ ### Default format (`:json` / `:html`)
56
+
57
+ ```ruby
58
+ # App-wide (lambda/config/environment.rb)
59
+ Belt.configure do |c|
60
+ c.default_format = :json # default — API first
61
+ end
62
+
63
+ # Per-controller override (inherits down the chain)
64
+ class PagesController < ApplicationController
65
+ self.default_format = :html
66
+ end
67
+ ```
68
+
69
+ | `default_format` | Implicit response (no explicit helper) |
70
+ |---|---|
71
+ | `:json` (default) | `success_response` from assigns |
72
+ | `:html` | `render` template `views/<controller>/<action>.html.erb` (missing → `TemplateNotFound`) |
73
+
74
+ ### Symbol status codes, `head`, `response_status`
75
+
76
+ ```ruby
77
+ success_response({ post: post.to_h }, :created) # 201
78
+ error_response("Nope", :unprocessable_entity) # 422
79
+ head :no_content # 204 empty body
80
+ head :created # 201 empty body
81
+
82
+ def create
83
+ @post = Post.create!(...)
84
+ response_status :created # 201 + implicit assigns body
85
+ end
86
+ ```
87
+
88
+ Bare integers still work. Symbols match the Rack/Rails names (`:created`, `:not_found`, …).
89
+
3
90
  ## 0.3.0
4
91
 
5
92
  ### Pre-Deploy Backups
data/README.md CHANGED
@@ -71,24 +71,31 @@ require "belt"
71
71
  class PostsController < BeltController::Base
72
72
  before_action :authenticate!
73
73
 
74
+ # Implicit response: assigns become the JSON body
75
+ # → { "posts": [ { "id": "...", "title": "..." }, ... ] }
74
76
  def index
75
- posts = Post.where(user_id: current_user_id, index: "UserIndex")
76
- success_response(posts.map(&:attributes))
77
+ @posts = Post.where(user_id: current_user_id, index: "UserIndex")
77
78
  end
78
79
 
79
80
  def show
80
- post = Post.find(params["id"])
81
- success_response(post.attributes)
81
+ @post = Post.find(params["id"])
82
82
  end
83
83
 
84
+ # Implicit body + non-200 status
84
85
  def create
85
86
  attrs = params.require(:post).permit(:title, :body).to_h
86
- post = Post.create!(attrs.merge(user_id: current_user_id))
87
- success_response(post.attributes, 201)
87
+ @post = Post.create!(attrs.merge(user_id: current_user_id))
88
+ response_status :created
89
+ end
90
+
91
+ def destroy
92
+ Post.find(params["id"]).destroy
93
+ head :no_content
88
94
  end
89
95
  end
90
96
  ```
91
97
 
98
+
92
99
  ### 4. Lambda entry point
93
100
 
94
101
  Use `Belt::LambdaHandler` to get automatic observability, CORS preflight handling, and error wrapping:
@@ -199,9 +206,42 @@ end
199
206
 
200
207
  ```ruby
201
208
  success_response({ id: "123", name: "Example" }) # 200 JSON with CORS
202
- success_response({ id: "123" }, 201) # 201 Created
203
- error_response("Not found", 404) # 404 JSON error
209
+ success_response({ id: "123" }, :created) # 201 Created (symbol or int)
210
+ error_response("Not found", :not_found) # 404 JSON error
211
+ error_response("Nope", :unprocessable_entity) # 422
204
212
  html_response("<h1>Hello</h1>") # 200 HTML with CORS
213
+ head :no_content # 204 empty body
214
+ head :created # 201 empty body
215
+ ```
216
+
217
+ ### Default format (API vs HTML)
218
+
219
+ ```ruby
220
+ # App-wide — typically in lambda/config/environment.rb
221
+ Belt.configure do |config|
222
+ config.default_format = :json # default
223
+ end
224
+
225
+ # Per-controller override
226
+ class PagesController < ApplicationController
227
+ self.default_format = :html
228
+ end
229
+ ```
230
+
231
+ - **`:json`** (default): action assigns → `success_response({ posts: [...] })`
232
+ - **`:html`**: action assigns stay on the controller; Belt implicitly `render`s
233
+ `views/<controller>/<action>.html.erb`. Missing template raises `Belt::TemplateNotFound`
234
+ (no silent JSON fallback).
235
+ - Explicit helpers always win: `success_response`, `error_response`, `html_response`,
236
+ `render`, `head`.
237
+
238
+ ### Non-200 with implicit assigns
239
+
240
+ ```ruby
241
+ def create
242
+ @post = Post.create!(...)
243
+ response_status :created # → 201 + { post: {...} }
244
+ end
205
245
  ```
206
246
 
207
247
  ## Controller Discovery
@@ -165,7 +165,7 @@ module Belt
165
165
  def error_response(message, status_code, event = nil)
166
166
  origin = Belt::Helpers::CorsOrigin.resolve_origin(Belt::Helpers::CorsOrigin.origin_from_event(event))
167
167
  headers = {
168
- 'Access-Control-Allow-Headers' => 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
168
+ 'Access-Control-Allow-Headers' => 'Content-Type,Accept,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
169
169
  'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
170
170
  'Content-Type' => 'application/json'
171
171
  }
@@ -1,82 +1,136 @@
1
1
  * { margin: 0; padding: 0; box-sizing: border-box; }
2
+ html, body {
3
+ height: 100%;
4
+ }
2
5
  body {
3
6
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
4
- background: #f8f9fa;
7
+ background: #0b1220;
5
8
  color: #2d3748;
6
9
  min-height: 100vh;
10
+ overflow: hidden;
7
11
  }
8
12
 
9
- /* Hero sectionfull width background image */
13
+ /* Hero fills the viewport all content lives in the overlay (no scroll) */
10
14
  .hero {
11
15
  position: relative;
12
16
  width: 100%;
17
+ height: 100vh;
13
18
  overflow: hidden;
14
19
  }
15
20
  .hero-bg {
16
21
  width: 100%;
17
- height: auto;
22
+ height: 100%;
23
+ object-fit: cover;
24
+ /* Pin the logo / sky — crop hills at the bottom if the viewport is short */
25
+ object-position: top center;
18
26
  display: block;
19
27
  }
20
28
  .overlay {
21
29
  position: absolute;
22
- top: 35%;
30
+ top: 50%;
23
31
  left: 50%;
24
32
  transform: translate(-50%, -50%);
25
- padding: 2rem 3rem;
26
- background: rgba(255, 255, 255, 0.50);
27
- backdrop-filter: blur(6px);
28
- -webkit-backdrop-filter: blur(6px);
29
- border-radius: 12px;
33
+ padding: 1.35rem 1.6rem 1.25rem;
34
+ background: rgba(255, 255, 255, 0.55);
35
+ backdrop-filter: blur(8px);
36
+ -webkit-backdrop-filter: blur(8px);
37
+ border-radius: 14px;
30
38
  text-align: center;
31
- max-width: 600px;
32
- width: 90%;
33
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
39
+ max-width: 560px;
40
+ width: min(92vw, 560px);
41
+ max-height: min(92vh, 620px);
42
+ overflow: auto;
43
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
34
44
  }
35
45
  .overlay h1 {
36
- font-size: 2.2rem;
46
+ font-size: 1.85rem;
37
47
  font-weight: 700;
38
48
  color: #2d3748;
39
- margin-bottom: 0.4rem;
49
+ margin-bottom: 0.3rem;
50
+ line-height: 1.2;
40
51
  }
41
52
  .overlay .subtitle {
42
- font-size: 1.1rem;
53
+ font-size: 0.95rem;
43
54
  color: #4a5568;
55
+ line-height: 1.4;
44
56
  }
45
57
 
46
- /* Content below the hero */
47
- .container {
48
- max-width: 700px;
49
- margin: 0 auto;
50
- padding: 2rem;
51
- }
58
+ /* Stack + next steps sit inside the white card */
52
59
  .stack-check {
53
60
  display: flex;
54
61
  align-items: center;
55
62
  justify-content: center;
56
- gap: 0.75rem;
57
- margin-bottom: 2rem;
63
+ gap: 0.4rem;
64
+ margin: 1rem 0 0.85rem;
58
65
  flex-wrap: wrap;
59
66
  }
60
67
  .check-item {
61
68
  display: flex;
62
69
  align-items: center;
63
- gap: 0.4rem;
64
- padding: 0.5rem 1rem;
70
+ gap: 0.35rem;
71
+ padding: 0.35rem 0.7rem;
65
72
  border-radius: 8px;
66
73
  font-weight: 500;
74
+ font-size: 0.88rem;
67
75
  }
68
76
  .check-pass {
69
- background: rgba(39, 174, 96, 0.1);
70
- border: 1px solid rgba(39, 174, 96, 0.3);
77
+ background: rgba(39, 174, 96, 0.12);
78
+ border: 1px solid rgba(39, 174, 96, 0.35);
71
79
  color: #1e8449;
72
80
  }
81
+ /* Optional / empty DynamoDB — not an error, just not in use yet */
82
+ .check-neutral {
83
+ background: rgba(113, 128, 150, 0.1);
84
+ border: 1px solid rgba(113, 128, 150, 0.28);
85
+ color: #4a5568;
86
+ }
73
87
  .check-warn {
74
88
  background: rgba(243, 156, 18, 0.1);
75
89
  border: 1px solid rgba(243, 156, 18, 0.3);
76
90
  color: #d68910;
77
91
  }
78
- .icon { font-size: 1.1rem; }
79
- .arrow { color: #718096; font-size: 1.2rem; }
92
+ .icon { font-size: 1rem; }
93
+ .arrow { color: #718096; font-size: 1.05rem; }
94
+
95
+ .next-steps {
96
+ text-align: left;
97
+ border-top: 1px solid rgba(45, 55, 72, 0.12);
98
+ padding-top: 0.75rem;
99
+ margin-top: 0.15rem;
100
+ }
101
+ .next-steps h2 {
102
+ font-size: 0.92rem;
103
+ margin-bottom: 0.45rem;
104
+ color: #2d3748;
105
+ text-align: left;
106
+ }
107
+ .next-steps ol {
108
+ padding-left: 1.2rem;
109
+ margin: 0;
110
+ }
111
+ .next-steps li {
112
+ margin-bottom: 0.4rem;
113
+ color: #4a5568;
114
+ line-height: 1.4;
115
+ font-size: 0.88rem;
116
+ }
117
+ .next-steps li:last-child { margin-bottom: 0; }
118
+ .next-steps code,
119
+ .info code {
120
+ background: rgba(99, 45, 145, 0.1);
121
+ padding: 0.12rem 0.35rem;
122
+ border-radius: 4px;
123
+ color: #632d91;
124
+ font-size: 0.78rem;
125
+ word-break: break-word;
126
+ }
127
+
128
+ /* Legacy blocks (local server welcome, older layouts) */
129
+ .container {
130
+ max-width: 700px;
131
+ margin: 0 auto;
132
+ padding: 2rem;
133
+ }
80
134
  .info {
81
135
  background: #ffffff;
82
136
  border: 1px solid #e2e8f0;
@@ -87,26 +141,44 @@ body {
87
141
  .info p { margin-bottom: 0.5rem; color: #4a5568; }
88
142
  .info p:last-child { margin-bottom: 0; }
89
143
  .info strong { color: #2d3748; }
90
- .info code {
91
- background: rgba(99, 45, 145, 0.08);
92
- padding: 0.15rem 0.4rem;
144
+ .dynamodb-hint {
145
+ text-align: center;
146
+ margin: 0.5rem 0;
147
+ color: #4a5568;
148
+ font-size: 0.9rem;
149
+ line-height: 1.45;
150
+ }
151
+ .dynamodb-hint code {
152
+ background: rgba(99, 45, 145, 0.1);
153
+ padding: 0.12rem 0.35rem;
93
154
  border-radius: 4px;
94
155
  color: #632d91;
95
- font-size: 0.85rem;
156
+ font-size: 0.8rem;
157
+ word-break: break-word;
96
158
  }
97
- .next-steps {
98
- background: #ffffff;
99
- border: 1px solid #e2e8f0;
100
- border-radius: 12px;
101
- padding: 1.5rem;
159
+ .footer-note {
160
+ margin-top: 0.75rem;
161
+ padding-top: 0.65rem;
162
+ border-top: 1px solid rgba(45, 55, 72, 0.1);
163
+ font-size: 0.78rem;
164
+ color: #718096;
165
+ text-align: center;
102
166
  }
103
- .next-steps h2 { font-size: 1.1rem; margin-bottom: 1rem; color: #2d3748; }
104
- .next-steps ol { padding-left: 1.5rem; }
105
- .next-steps li { margin-bottom: 0.6rem; color: #4a5568; line-height: 1.5; }
106
- .next-steps code {
107
- background: rgba(99, 45, 145, 0.08);
108
- padding: 0.15rem 0.4rem;
109
- border-radius: 4px;
110
- color: #632d91;
111
- font-size: 0.85rem;
167
+ .footer-note code {
168
+ background: rgba(45, 55, 72, 0.06);
169
+ padding: 0.08rem 0.3rem;
170
+ border-radius: 3px;
171
+ color: #4a5568;
172
+ }
173
+
174
+ /* Tiny viewports: shrink type a bit so everything still fits */
175
+ @media (max-height: 640px) {
176
+ .overlay {
177
+ padding: 1rem 1.15rem;
178
+ }
179
+ .overlay h1 { font-size: 1.5rem; }
180
+ .overlay .subtitle { font-size: 0.88rem; }
181
+ .stack-check { margin: 0.7rem 0 0.55rem; }
182
+ .check-item { padding: 0.28rem 0.55rem; font-size: 0.8rem; }
183
+ .next-steps li { font-size: 0.82rem; }
112
184
  }
@@ -71,20 +71,26 @@ module Belt
71
71
  def harden_bucket(bucket, audit)
72
72
  unless audit[:versioning]
73
73
  enable_versioning(bucket)
74
- puts ' enable versioning'
74
+ say_security ' enable versioning'
75
75
  end
76
76
  unless audit[:encryption]
77
77
  enable_encryption(bucket)
78
- puts ' enable AES-256 encryption'
78
+ say_security ' enable AES-256 encryption'
79
79
  end
80
80
  unless audit[:public_access_block]
81
81
  block_public_access(bucket)
82
- puts ' enable public access block'
82
+ say_security ' enable public access block'
83
83
  end
84
84
  return if audit[:tls_policy]
85
85
 
86
86
  apply_tls_policy(bucket)
87
- puts ' enable TLS-only bucket policy'
87
+ say_security ' enable TLS-only bucket policy'
88
+ end
89
+
90
+ def say_security(message)
91
+ return if respond_to?(:say, true) && @quiet
92
+
93
+ puts message
88
94
  end
89
95
 
90
96
  private
@@ -9,6 +9,7 @@ require_relative 'terraform_command'
9
9
  require_relative 'backup_config'
10
10
  require_relative 'backup_runner'
11
11
  require_relative 'environment_config'
12
+ require_relative 'path_gem_materializer'
12
13
 
13
14
  module Belt
14
15
  module CLI
@@ -150,6 +151,7 @@ module Belt
150
151
  puts "belt → deploying #{@env} (in #{env_dir}/)\n\n"
151
152
 
152
153
  ensure_lockfile_consistent!
154
+ warn_active_path_gems!
153
155
  generate_routes_if_needed
154
156
  run_backups unless @skip_backup
155
157
 
@@ -457,9 +459,55 @@ module Belt
457
459
  FileUtils.cp(gemfile, build_dir) if File.exist?(gemfile)
458
460
  FileUtils.cp(lockfile, build_dir) if File.exist?(lockfile)
459
461
 
462
+ copy_vendor_cache(build_dir)
463
+ materialize_path_gems!(build_dir)
464
+
460
465
  puts ' 📁 Copied handlers, controllers, models, lib, config'
461
466
  end
462
467
 
468
+ # Stage prebuilt .gem files so Docker `bundle install` can install unreleased
469
+ # gems as normal package installs (with specifications/), not path/git layouts.
470
+ # Prefer project-root vendor/cache (next to Gemfile — Bundler's natural path);
471
+ # fall back to lambda/vendor/cache for older layouts.
472
+ def copy_vendor_cache(build_dir)
473
+ cache_dir = [
474
+ File.join(@project_root, 'vendor', 'cache'),
475
+ File.join(@project_root, 'lambda', 'vendor', 'cache')
476
+ ].find { |dir| Dir.exist?(dir) && !Dir.empty?(dir) }
477
+ return unless cache_dir
478
+
479
+ dest = File.join(build_dir, 'vendor', 'cache')
480
+ FileUtils.mkdir_p(dest)
481
+ FileUtils.cp_r(Dir.glob(File.join(cache_dir, '*')), dest)
482
+ gem_count = Dir.glob(File.join(dest, '*.gem')).size
483
+ puts " 📦 Copied vendor/cache (#{gem_count} local gem(s))"
484
+ end
485
+
486
+ # path: gems install under bundler/gems/ with no specifications/ — Lambda's
487
+ # bare `require 'belt'` can't see them. Build real .gem files into the
488
+ # package's vendor/cache and pin versions in the *build* Gemfile/lock only.
489
+ def materialize_path_gems!(build_dir)
490
+ gems = PathGemMaterializer.materialize!(build_dir, project_root: @project_root)
491
+ return if gems.empty?
492
+
493
+ puts " 🔧 Materialized path gem(s) → vendor/cache: #{gems.join(', ')}"
494
+ end
495
+
496
+ # path: gems need host-side materialize before Docker install. --rebuild
497
+ # always does it. Full terraform apply needs a conveyor-belt build that
498
+ # includes path-gem materialize (discord-vendor-cache-gemfile-parent+).
499
+ def warn_active_path_gems!
500
+ lockfile = File.join(@project_root, 'Gemfile.lock')
501
+ return unless File.exist?(lockfile)
502
+ return unless File.read(lockfile).match?(/^PATH\n/)
503
+
504
+ puts ' ⚠ Gemfile.lock has PATH gems (path: in Gemfile).'
505
+ puts " Safe now: belt deploy #{@env} --rebuild (always materializes)."
506
+ puts ' Full terraform apply needs conveyor-belt with path-gem materialize.'
507
+ puts ' Or pin a version + drop a built .gem in vendor/cache/'
508
+ puts ''
509
+ end
510
+
463
511
  def build_gems(build_dir)
464
512
  puts ' 🐳 Building gems in Docker (Lambda-compatible)...'
465
513