belt 0.2.10 → 0.2.11

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,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'erb'
5
+
6
+ module Belt
7
+ module CLI
8
+ # Scaffold a new Belt plugin gem (similar to `rails plugin new`).
9
+ #
10
+ # Usage:
11
+ # belt plugin new messaging
12
+ # belt plugin new belt-pay --path ~/code
13
+ #
14
+ # Creates a gem that registers with Belt's GeneratorRegistry via:
15
+ # lib/belt/generators/<name>_generator.rb
16
+ class PluginCommand
17
+ TEMPLATE_DIR = File.expand_path('../../templates/plugin', __dir__)
18
+
19
+ def self.run(args)
20
+ subcommand = args.shift
21
+
22
+ case subcommand
23
+ when 'new'
24
+ new(args).generate
25
+ when nil, '--help', '-h', 'help'
26
+ print_help
27
+ else
28
+ puts "Unknown plugin subcommand: #{subcommand}"
29
+ puts ''
30
+ print_help
31
+ exit 1
32
+ end
33
+ end
34
+
35
+ def self.print_help
36
+ puts <<~HELP
37
+ Manage Belt plugin gems.
38
+
39
+ Usage:
40
+ belt plugin new <name> [options]
41
+
42
+ Arguments:
43
+ name Plugin name (e.g. messaging, pay, or belt-messaging)
44
+
45
+ Options:
46
+ --path DIR Parent directory for the gem (default: current directory)
47
+ --summary TEXT Short gem summary for the gemspec
48
+ --force Overwrite files if the directory already exists
49
+ -h, --help Show this help
50
+
51
+ Examples:
52
+ belt plugin new messaging
53
+ belt plugin new pay --path ~/Code
54
+ belt plugin new belt-notifications --summary "Push notifications for Belt apps"
55
+
56
+ What you get:
57
+ A standalone gem (belt-<name>) with:
58
+ - Module under Belt::<Name>
59
+ - Generator at lib/belt/generators/<name>_generator.rb
60
+ - Configuration stub, version, RSpec, README, AGENTS.md
61
+ - Hooked into `belt generate <name>` / `belt destroy <name>` once
62
+ the gem is in an app's Gemfile
63
+
64
+ Reference plugins:
65
+ belt-messaging — SMS via AWS End User Messaging
66
+ belt-pay — Stripe payments and subscriptions
67
+
68
+ See the Belt README (Plugins / Contributing) and AGENTS.md for the full guide.
69
+ HELP
70
+ end
71
+
72
+ def initialize(args)
73
+ @raw_name = nil
74
+ @path = Dir.pwd
75
+ @summary = nil
76
+ @force = false
77
+
78
+ i = 0
79
+ while i < args.length
80
+ arg = args[i]
81
+ case arg
82
+ when '--path'
83
+ i += 1
84
+ @path = args[i] || Dir.pwd
85
+ when /^--path=/
86
+ @path = arg.split('=', 2).last
87
+ when '--summary'
88
+ i += 1
89
+ @summary = args[i]
90
+ when /^--summary=/
91
+ @summary = arg.split('=', 2).last
92
+ when '--force'
93
+ @force = true
94
+ when '--help', '-h'
95
+ self.class.print_help
96
+ exit 0
97
+ else
98
+ if arg.start_with?('-')
99
+ puts "Unknown option: #{arg}"
100
+ exit 1
101
+ end
102
+ @raw_name ||= arg
103
+ end
104
+ i += 1
105
+ end
106
+ end
107
+
108
+ def generate
109
+ if @raw_name.nil? || @raw_name.strip.empty?
110
+ puts 'Usage: belt plugin new <name> [options]'
111
+ puts "Run 'belt plugin --help' for more information."
112
+ exit 1
113
+ end
114
+
115
+ normalize_names!
116
+
117
+ if Dir.exist?(@gem_dir) && !@force
118
+ puts "✗ Directory already exists: #{@gem_dir}"
119
+ puts ' Use --force to overwrite, or choose a different name/path.'
120
+ exit 1
121
+ end
122
+
123
+ FileUtils.mkdir_p(@gem_dir)
124
+ write_files
125
+ print_success
126
+ end
127
+
128
+ private
129
+
130
+ def normalize_names!
131
+ # Accept "messaging", "belt-messaging", "Belt::Messaging"
132
+ name = @raw_name.to_s.strip
133
+ name = name.sub(/\ABelt::/i, '')
134
+ name = name.gsub('::', '-')
135
+ name = name.sub(/\Abelt[-_]/i, '')
136
+ name = name.gsub(/[^a-z0-9_-]/i, '_').downcase.tr('_', '-')
137
+ name = name.gsub(/-+/, '-').gsub(/\A-|-\z/, '')
138
+
139
+ if name.empty?
140
+ puts "✗ Invalid plugin name: #{@raw_name.inspect}"
141
+ exit 1
142
+ end
143
+
144
+ @plugin_name = name # messaging
145
+ @gem_name = "belt-#{name}" # belt-messaging
146
+ @module_name = name.split('-').map(&:capitalize).join # Messaging
147
+ @constant_path = "Belt::#{@module_name}" # Belt::Messaging
148
+ @generator_class = "#{@module_name}Generator"
149
+ @summary ||= "#{@module_name} plugin for Belt applications"
150
+ @gem_dir = File.expand_path(File.join(@path, @gem_name))
151
+ end
152
+
153
+ def write_files
154
+ files.each do |relative, content|
155
+ full = File.join(@gem_dir, relative)
156
+ FileUtils.mkdir_p(File.dirname(full))
157
+ File.write(full, content)
158
+ puts " create #{File.join(@gem_name, relative)}"
159
+ end
160
+ end
161
+
162
+ def files
163
+ gen = "lib/belt/generators/#{@plugin_name.tr('-', '_')}_generator.rb"
164
+ {
165
+ "#{@gem_name}.gemspec" => render('gemspec.erb'),
166
+ 'Gemfile' => render('Gemfile.erb'),
167
+ 'Rakefile' => render('Rakefile.erb'),
168
+ 'README.md' => render('README.md.erb'),
169
+ 'AGENTS.md' => render('AGENTS.md.erb'),
170
+ 'CHANGELOG.md' => render('CHANGELOG.md.erb'),
171
+ 'LICENSE' => render('LICENSE.erb'),
172
+ '.gitignore' => render('gitignore.erb'),
173
+ '.rspec' => render('rspec.erb'),
174
+ "lib/#{@gem_name}.rb" => render('lib/entry.rb.erb'),
175
+ "lib/belt/#{@plugin_name.tr('-', '_')}.rb" => render('lib/belt/module.rb.erb'),
176
+ "lib/belt/#{@plugin_name.tr('-', '_')}/version.rb" => render('lib/belt/module/version.rb.erb'),
177
+ "lib/belt/#{@plugin_name.tr('-', '_')}/configuration.rb" => render('lib/belt/module/configuration.rb.erb'),
178
+ gen => render('lib/belt/generators/generator.rb.erb'),
179
+ "lib/belt/#{@plugin_name.tr('-', '_')}/templates/.gitkeep" => '',
180
+ 'spec/spec_helper.rb' => render('spec/spec_helper.rb.erb'),
181
+ "spec/belt/#{@plugin_name.tr('-', '_')}/configuration_spec.rb" => render('spec/configuration_spec.rb.erb')
182
+ }
183
+ end
184
+
185
+ def render(template_name)
186
+ path = File.join(TEMPLATE_DIR, template_name)
187
+ raise "Missing plugin template: #{path}" unless File.exist?(path)
188
+
189
+ erb = ERB.new(File.read(path), trim_mode: '-')
190
+ # Expose locals used in templates
191
+ gem_name = @gem_name
192
+ plugin_name = @plugin_name
193
+ module_name = @module_name
194
+ constant_path = @constant_path
195
+ generator_class = @generator_class
196
+ summary = @summary
197
+ underscored = @plugin_name.tr('-', '_')
198
+ erb.result(binding)
199
+ end
200
+
201
+ def print_success
202
+ puts ''
203
+ puts "✓ Created plugin gem #{@gem_name}"
204
+ puts ''
205
+ puts 'Next steps:'
206
+ puts " 1. cd #{@gem_dir}"
207
+ puts ' 2. bundle install'
208
+ puts ' 3. Implement your library under lib/belt/'
209
+ puts " 4. Flesh out lib/belt/generators/#{@plugin_name.tr('-', '_')}_generator.rb"
210
+ puts ' (templates go in lib/belt/<name>/templates/)'
211
+ puts ' 5. In a Belt app Gemfile:'
212
+ puts " gem \"#{@gem_name}\", path: \"#{@gem_dir}\""
213
+ puts ' 6. bundle install && belt generate --help'
214
+ puts " → should list \"#{@plugin_name}\" under Gem Generators"
215
+ puts " 7. belt generate #{@plugin_name}"
216
+ puts ''
217
+ puts 'Reference implementations: belt-messaging, belt-pay'
218
+ end
219
+ end
220
+ end
221
+ end
data/lib/belt/cli.rb CHANGED
@@ -22,6 +22,7 @@ require_relative 'cli/lambda_config_command'
22
22
  require_relative 'cli/tasks_command'
23
23
  require_relative 'cli/console_command'
24
24
  require_relative 'cli/doctor_command'
25
+ require_relative 'cli/plugin_command'
25
26
 
26
27
  module Belt
27
28
  module CLI
@@ -35,6 +36,7 @@ module Belt
35
36
  %w[tasks --tasks -T] => Belt::CLI::TasksCommand,
36
37
  'setup' => Belt::CLI::SetupCommand,
37
38
  'doctor' => Belt::CLI::DoctorCommand,
39
+ 'plugin' => Belt::CLI::PluginCommand,
38
40
  'deploy' => Belt::CLI::DeployCommand,
39
41
  'frontend' => Belt::CLI::FrontendEnvCommand,
40
42
  %w[server s] => Belt::CLI::ServerCommand,
@@ -117,6 +119,7 @@ module Belt
117
119
  setup tables <env> Generate DynamoDB tables from schema
118
120
  setup frontend <env> Generate S3 + CloudFront infrastructure
119
121
  doctor Check system dependencies and AWS config
122
+ plugin new <name> Scaffold a new Belt plugin gem
120
123
  init [environment] <env> terraform init for environment
121
124
  plan [environment] <env> terraform plan for environment
122
125
  apply [environment] <env> terraform apply for environment
@@ -149,6 +152,7 @@ module Belt
149
152
  belt apply wups
150
153
  belt tasks # list all rake tasks
151
154
  belt lambda:build_layer # run a rake task directly
155
+ belt plugin new messaging # scaffold a belt-messaging style plugin gem
152
156
  USAGE
153
157
  end
154
158
  end
@@ -3,10 +3,16 @@
3
3
  module Belt
4
4
  module Helpers
5
5
  module CorsOrigin
6
+ # Maximum length for an Origin header to prevent ReDoS or memory abuse.
7
+ MAX_ORIGIN_LENGTH = 253
8
+
6
9
  def self.resolve_origin(request_origin)
7
10
  allowed = allowed_origins
8
11
  return nil if allowed.empty?
9
- return request_origin if request_origin && matches_allowed?(request_origin, allowed)
12
+
13
+ if request_origin && valid_origin?(request_origin) && matches_allowed?(request_origin, allowed)
14
+ return request_origin
15
+ end
10
16
 
11
17
  # Don't return a wildcard pattern as a literal origin — use '*' instead
12
18
  first = allowed.first
@@ -18,7 +24,7 @@ module Belt
18
24
 
19
25
  allowed.any? do |pattern|
20
26
  if pattern.include?('*')
21
- regex = Regexp.new("\\A#{Regexp.escape(pattern).gsub('\*', '[^.]+')}\\z")
27
+ regex = Regexp.new("\\A#{Regexp.escape(pattern).gsub('\*', '[a-z0-9\\-]+')}\\z")
22
28
  regex.match?(origin)
23
29
  else
24
30
  pattern == origin
@@ -26,6 +32,17 @@ module Belt
26
32
  end
27
33
  end
28
34
 
35
+ # Validate that the origin looks like a legitimate URL scheme+host.
36
+ # Rejects origins with path components, whitespace, or unexpected characters.
37
+ def self.valid_origin?(origin)
38
+ return false if origin.nil? || origin.empty?
39
+ return false if origin.length > MAX_ORIGIN_LENGTH
40
+ return false unless origin.match?(%r{\A https?://[a-z0-9\-.:]+\z}ix)
41
+
42
+ # Reject origins with user-info, paths, queries, or fragments
43
+ !origin.include?('@') && !origin.include?('?') && !origin.include?('#')
44
+ end
45
+
29
46
  def self.origin_from_event(event)
30
47
  return nil unless event.is_a?(Hash)
31
48
 
@@ -7,6 +7,10 @@ require_relative '../http_status'
7
7
  module Belt
8
8
  module Helpers
9
9
  module Response
10
+ # Maximum request body size (10 MB). Bodies larger than this are rejected
11
+ # to prevent memory exhaustion attacks on Lambda functions.
12
+ MAX_REQUEST_BODY_SIZE = 10 * 1024 * 1024
13
+
10
14
  def cors_headers(event = nil)
11
15
  event = @event if event.nil? && instance_variable_defined?(:@event)
12
16
  origin = CorsOrigin.resolve_origin(CorsOrigin.origin_from_event(event))
@@ -15,7 +19,9 @@ module Belt
15
19
  'Access-Control-Allow-Headers' => allow_headers,
16
20
  'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,PATCH,OPTIONS',
17
21
  'Access-Control-Max-Age' => '300',
18
- 'Content-Type' => 'application/json'
22
+ 'Content-Type' => 'application/json',
23
+ 'X-Content-Type-Options' => 'nosniff',
24
+ 'Vary' => 'Origin'
19
25
  }
20
26
  headers['Access-Control-Allow-Origin'] = origin if origin
21
27
  headers
@@ -38,7 +44,12 @@ module Belt
38
44
  def html_response(html, status_code = 200)
39
45
  event = @event if instance_variable_defined?(:@event)
40
46
  origin = CorsOrigin.resolve_origin(CorsOrigin.origin_from_event(event))
41
- headers = { 'Content-Type' => 'text/html; charset=utf-8' }
47
+ headers = {
48
+ 'Content-Type' => 'text/html; charset=utf-8',
49
+ 'X-Content-Type-Options' => 'nosniff',
50
+ 'X-Frame-Options' => 'DENY',
51
+ 'Referrer-Policy' => 'strict-origin-when-cross-origin'
52
+ }
42
53
  headers['Access-Control-Allow-Origin'] = origin if origin
43
54
  { statusCode: resolve_status(status_code), headers: headers, body: html }
44
55
  end
@@ -76,11 +76,8 @@ module Belt
76
76
 
77
77
  return { statusCode: 200, headers: cors_headers(event), body: '{}' } if event['httpMethod'] == 'OPTIONS'
78
78
 
79
- begin
80
- body = JSON.parse(event['body'] || '{}')
81
- rescue JSON::ParserError
82
- return error_response('Invalid JSON in request body')
83
- end
79
+ body = parse_request_body(event)
80
+ return body if body.is_a?(Hash) && body[:statusCode]
84
81
 
85
82
  begin
86
83
  result = execute(path: event['path'], body: body, event: event)
@@ -107,6 +104,18 @@ module Belt
107
104
 
108
105
  private
109
106
 
107
+ def parse_request_body(event)
108
+ raw_body = event['body'] || '{}'
109
+
110
+ if raw_body.bytesize > Belt::Helpers::Response::MAX_REQUEST_BODY_SIZE
111
+ return error_response('Request body too large', 413)
112
+ end
113
+
114
+ JSON.parse(raw_body)
115
+ rescue JSON::ParserError
116
+ error_response('Invalid JSON in request body', 400)
117
+ end
118
+
110
119
  def init_observability(context:)
111
120
  service_name = ENV['ACTION'] || context.function_name.split('-').last
112
121
 
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.10'
4
+ VERSION = '0.2.11'
5
5
  end
@@ -1,7 +1,6 @@
1
1
  terraform {
2
2
  backend "s3" {
3
- # Resolved to belt-terraform-state-<account-id> by `belt setup state` / `belt new`
4
- bucket = "belt-terraform-state"
3
+ bucket = "<%= @state_bucket %>"
5
4
  key = "<%= s3_safe_name(@app_name) %>/<%= @env_name %>/terraform.tfstate"
6
5
  region = "us-east-1"
7
6
  encrypt = true
@@ -0,0 +1,135 @@
1
+ # AGENTS.md — <%= gem_name %>
2
+
3
+ This file is for AI agents (and humans) working **on this Belt plugin gem**, not on a host Belt app.
4
+
5
+ Host apps get their own `AGENTS.md` from `belt new`. Core belt docs live in the [belt](https://github.com/stowzilla/belt) repo (`README.md` + root `AGENTS.md`).
6
+
7
+ ## What this gem is
8
+
9
+ **<%= gem_name %>** is a Belt plugin: optional capability that installs into Belt apps via the generator API.
10
+
11
+ - **Runtime API** lives in the gem (`<%= constant_path %>`) — apps require the gem and call it directly
12
+ - **Generator** copies only what the host app must own (Terraform, Lambda entrypoints, optional overrides)
13
+ - After the gem is in an app's Gemfile, `belt generate <%= plugin_name %>` / `belt destroy <%= plugin_name %>` work automatically
14
+
15
+ ## Stack
16
+
17
+ | Piece | Role |
18
+ |-------|------|
19
+ | **belt** | Host framework + CLI; discovers this generator via `GeneratorRegistry` |
20
+ | **<%= gem_name %>** (this repo) | Plugin runtime + `belt generate <%= plugin_name %>` |
21
+ | Host Belt app | Where Terraform / Lambda config land after generation |
22
+
23
+ ## Layout
24
+
25
+ ```
26
+ <%= gem_name %>/
27
+ ├── <%= gem_name %>.gemspec
28
+ ├── lib/
29
+ │ ├── <%= gem_name %>.rb # require entrypoint
30
+ │ └── belt/
31
+ │ ├── <%= underscored %>.rb # <%= constant_path %> API
32
+ │ ├── <%= underscored %>/
33
+ │ │ ├── configuration.rb
34
+ │ │ ├── version.rb
35
+ │ │ └── templates/ # ERB used by the generator
36
+ │ └── generators/
37
+ │ └── <%= underscored %>_generator.rb # ← auto-discovered
38
+ ├── spec/
39
+ ├── README.md
40
+ └── AGENTS.md # This file
41
+ ```
42
+
43
+ **Do not rename** `lib/belt/generators/<%= underscored %>_generator.rb` or the class
44
+ `Belt::Generators::<%= generator_class %>` — discovery depends on that path and name.
45
+
46
+ ## Generator contract
47
+
48
+ Implemented in `lib/belt/generators/<%= underscored %>_generator.rb`:
49
+
50
+ | Method | Required? | Purpose |
51
+ |--------|-----------|---------|
52
+ | `.run(args)` | **Yes** | Install into the current Belt app |
53
+ | `.destroy(args)` | Recommended | Tear down what generate created |
54
+ | `.description` | Recommended | One-liner for `belt generate --help` |
55
+
56
+ Class must live in `Belt::Generators` and be named `<%= generator_class %>`.
57
+
58
+ Reference implementations (copy structure, don't invent a parallel one):
59
+
60
+ - `belt-messaging` — SMS via AWS End User Messaging
61
+ - `belt-pay` — Stripe payments & subscriptions
62
+
63
+ ## Generator checklist
64
+
65
+ When fleshing out the generator, typically install:
66
+
67
+ 1. **Terraform module** → `infrastructure/modules/<%= plugin_name %>/` (`main.tf`, `variables.tf`, `outputs.tf`)
68
+ 2. **Lambda config** → `config/lambda/<%= plugin_name %>.yml` (timeout, memory, env, triggers)
69
+ 3. **Lambda entrypoint** → `lambda/<%= plugin_name %>.rb` using `Belt::LambdaHandler`
70
+ 4. **Routes / schema** → inject into `config/routes.tf.rb` or schema files when needed
71
+ 5. **Optional overrides** → `--controllers` (or similar) for app-local subclasses — keep defaults in the gem
72
+ 6. **Destroy path** → `belt destroy <%= plugin_name %>` removes generated artifacts
73
+ 7. **Help** → `.description` plus `--help` / `-h` explaining files and next steps
74
+
75
+ **Principle:** runtime stays in the gem; generators copy only host-owned infra and optional overrides (same idea as `rails g devise:views`).
76
+
77
+ ## Local development
78
+
79
+ ```bash
80
+ bundle install
81
+ bundle exec rspec
82
+ ```
83
+
84
+ Wire into a Belt app while iterating:
85
+
86
+ ```ruby
87
+ # In the app Gemfile
88
+ gem "<%= gem_name %>", path: "../<%= gem_name %>"
89
+ ```
90
+
91
+ ```bash
92
+ cd /path/to/belt-app
93
+ bundle install
94
+ belt generate --help # should list "<%= plugin_name %>" under Gem Generators
95
+ belt generate <%= plugin_name %>
96
+ belt destroy <%= plugin_name %>
97
+ ```
98
+
99
+ `belt deploy` vendors `path:` gems into `vendor/cache` so Lambda packaging works without publishing.
100
+
101
+ ## Configuration pattern
102
+
103
+ ```ruby
104
+ <%= constant_path %>.configure do |config|
105
+ # config.option = value
106
+ end
107
+ ```
108
+
109
+ Extend `lib/belt/<%= underscored %>/configuration.rb` and document options in README.
110
+
111
+ ## Conventions
112
+
113
+ 1. Follow patterns from `belt-messaging` / `belt-pay` before inventing new structure
114
+ 2. Keep the generator idempotent where practical; support `--force` for overwrites
115
+ 3. Specs under `spec/` for configuration and any non-trivial generator logic
116
+ 4. User-facing changes → `CHANGELOG.md`
117
+ 5. Never commit secrets, API keys, or real AWS account IDs
118
+ 6. Prefer gem defaults + optional generate flags over dumping all code into the app
119
+
120
+ ## Where to look first
121
+
122
+ | Task | Start here |
123
+ |------|------------|
124
+ | Runtime API | `lib/belt/<%= underscored %>.rb`, `lib/belt/<%= underscored %>/` |
125
+ | What gets installed into apps | `lib/belt/generators/<%= underscored %>_generator.rb` |
126
+ | Generator ERB templates | `lib/belt/<%= underscored %>/templates/` |
127
+ | Version / gemspec | `lib/belt/<%= underscored %>/version.rb`, `<%= gem_name %>.gemspec` |
128
+ | How belt discovers this | belt gem: `Belt::CLI::GeneratorRegistry` |
129
+
130
+ ## Do not
131
+
132
+ - Move the generator file out of `lib/belt/generators/*_generator.rb`
133
+ - Require a manual initializer in the host app for registration — discovery is automatic
134
+ - Put host-app deploy runbooks here — those belong in the app's `AGENTS.md`
135
+ - Duplicate large runtime classes into generated app files unless the user opts in (e.g. `--controllers`)
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## 0.0.1
4
+
5
+ - Initial scaffold generated by `belt plugin new <%= plugin_name %>`
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ group :development, :test do
8
+ gem 'rake', '~> 13.0'
9
+ gem 'rspec', '~> 3.0'
10
+ gem 'rubocop', '~> 1.0', require: false
11
+ end
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) <%= Time.now.year %>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,63 @@
1
+ # <%= gem_name %>
2
+
3
+ <%= summary %>.
4
+
5
+ A [Belt](https://github.com/stowzilla/belt) plugin. Add the gem to a Belt app, then run the generator to install infrastructure and app wiring.
6
+
7
+ ## Installation
8
+
9
+ Add to your Belt app's Gemfile:
10
+
11
+ ```ruby
12
+ gem '<%= gem_name %>'
13
+ # Or during development:
14
+ # gem '<%= gem_name %>', path: '../<%= gem_name %>'
15
+ ```
16
+
17
+ Then:
18
+
19
+ ```bash
20
+ bundle install
21
+ belt generate <%= plugin_name %>
22
+ ```
23
+
24
+ ## What You Get
25
+
26
+ ### From the gem (no generation needed)
27
+
28
+ ```ruby
29
+ # Configure
30
+ <%= constant_path %>.configure do |config|
31
+ # config.option = value
32
+ end
33
+ ```
34
+
35
+ ### From the generator (`belt g <%= plugin_name %>`)
36
+
37
+ Edit `lib/belt/generators/<%= underscored %>_generator.rb` and templates under
38
+ `lib/belt/<%= underscored %>/templates/` to define what gets installed into apps.
39
+
40
+ Typical plugin generators create some combination of:
41
+
42
+ - **Terraform module** — `infrastructure/modules/<%= plugin_name %>/`
43
+ - **Lambda entry point** — `lambda/<%= plugin_name %>.rb`
44
+ - **Lambda config** — `config/lambda/<%= plugin_name %>.yml`
45
+ - **Route / schema injection** — updates to `routes.tf.rb` / `schema.tf.rb`
46
+ - **Optional controller overrides** — `belt g <%= plugin_name %> --controllers`
47
+
48
+ ## Development
49
+
50
+ ```bash
51
+ bundle install
52
+ bundle exec rspec
53
+ ```
54
+
55
+ ## Removing
56
+
57
+ ```bash
58
+ belt destroy <%= plugin_name %>
59
+ ```
60
+
61
+ ## License
62
+
63
+ MIT
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+
7
+ task default: :spec
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/belt/<%= underscored %>/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = '<%= gem_name %>'
7
+ spec.version = <%= constant_path %>::VERSION
8
+ spec.authors = ['Your Name']
9
+ spec.email = ['you@example.com']
10
+
11
+ spec.summary = '<%= summary %>'
12
+ spec.description = '<%= summary %>. A Belt plugin gem — add to a Belt app Gemfile, then run `belt generate <%= plugin_name %>`.'
13
+ spec.homepage = 'https://github.com/example/<%= gem_name %>'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.3'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = spec.homepage
19
+ spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
+ spec.metadata['rubygems_mfa_required'] = 'true'
21
+
22
+ spec.files = Dir['lib/**/*', 'LICENSE', 'README.md', 'CHANGELOG.md']
23
+ spec.require_paths = ['lib']
24
+
25
+ spec.add_dependency 'belt', '~> 0.2'
26
+ end
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+ *.gem
10
+ Gemfile.lock
11
+ .rspec_status