belt 0.2.10 → 0.2.12

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +63 -0
  3. data/README.md +169 -0
  4. data/lib/belt/action_router.rb +32 -4
  5. data/lib/belt/cli/app_detection.rb +19 -5
  6. data/lib/belt/cli/contracts_command.rb +142 -0
  7. data/lib/belt/cli/destroy_command.rb +193 -10
  8. data/lib/belt/cli/environment_command.rb +32 -0
  9. data/lib/belt/cli/generate_command.rb +5 -5
  10. data/lib/belt/cli/new_command.rb +2 -2
  11. data/lib/belt/cli/plugin_command.rb +221 -0
  12. data/lib/belt/cli/routes_command/schema_loader.rb +17 -7
  13. data/lib/belt/cli/routes_command.rb +7 -3
  14. data/lib/belt/cli/setup_command.rb +1 -1
  15. data/lib/belt/cli/views_command.rb +7 -2
  16. data/lib/belt/cli.rb +7 -0
  17. data/lib/belt/helpers/cors_origin.rb +19 -2
  18. data/lib/belt/helpers/response.rb +13 -2
  19. data/lib/belt/lambda_handler.rb +14 -5
  20. data/lib/belt/root.rb +12 -3
  21. data/lib/belt/route_dsl.rb +2 -2
  22. data/lib/belt/version.rb +1 -1
  23. data/lib/templates/environment/backend.tf.erb +1 -2
  24. data/lib/templates/frontend_infra/frontend.tf.erb +2 -1
  25. data/lib/templates/module/frontend.tf.erb +2 -1
  26. data/lib/templates/module/main.tf.erb +1 -1
  27. data/lib/templates/new_app/AGENTS.md.erb +4 -4
  28. data/lib/templates/new_app/README.md.erb +2 -2
  29. data/lib/templates/plugin/AGENTS.md.erb +135 -0
  30. data/lib/templates/plugin/CHANGELOG.md.erb +5 -0
  31. data/lib/templates/plugin/Gemfile.erb +11 -0
  32. data/lib/templates/plugin/LICENSE.erb +21 -0
  33. data/lib/templates/plugin/README.md.erb +63 -0
  34. data/lib/templates/plugin/Rakefile.erb +7 -0
  35. data/lib/templates/plugin/gemspec.erb +26 -0
  36. data/lib/templates/plugin/gitignore.erb +11 -0
  37. data/lib/templates/plugin/lib/belt/generators/generator.rb.erb +122 -0
  38. data/lib/templates/plugin/lib/belt/module/configuration.rb.erb +15 -0
  39. data/lib/templates/plugin/lib/belt/module/version.rb.erb +7 -0
  40. data/lib/templates/plugin/lib/belt/module.rb.erb +27 -0
  41. data/lib/templates/plugin/lib/entry.rb.erb +3 -0
  42. data/lib/templates/plugin/rspec.erb +3 -0
  43. data/lib/templates/plugin/spec/configuration_spec.rb.erb +17 -0
  44. data/lib/templates/plugin/spec/spec_helper.rb.erb +19 -0
  45. metadata +21 -3
  46. /data/lib/templates/new_app/config/{schema.tf.rb.erb → contracts.rb.erb} +0 -0
  47. /data/lib/templates/new_app/config/{routes.tf.rb.erb → routes.rb.erb} +0 -0
data/lib/belt/cli.rb CHANGED
@@ -18,10 +18,12 @@ require_relative 'cli/backup_config'
18
18
  require_relative 'cli/backup_runner'
19
19
  require_relative 'cli/server_command'
20
20
  require_relative 'cli/routes_command'
21
+ require_relative 'cli/contracts_command'
21
22
  require_relative 'cli/lambda_config_command'
22
23
  require_relative 'cli/tasks_command'
23
24
  require_relative 'cli/console_command'
24
25
  require_relative 'cli/doctor_command'
26
+ require_relative 'cli/plugin_command'
25
27
 
26
28
  module Belt
27
29
  module CLI
@@ -30,11 +32,13 @@ module Belt
30
32
  %w[generate g] => Belt::CLI::GenerateCommand,
31
33
  %w[destroy d] => Belt::CLI::DestroyCommand,
32
34
  'routes' => Belt::CLI::RoutesCommand,
35
+ 'contracts' => Belt::CLI::ContractsCommand,
33
36
  'lambda-config' => Belt::CLI::LambdaConfigCommand,
34
37
  %w[console c] => Belt::CLI::ConsoleCommand,
35
38
  %w[tasks --tasks -T] => Belt::CLI::TasksCommand,
36
39
  'setup' => Belt::CLI::SetupCommand,
37
40
  'doctor' => Belt::CLI::DoctorCommand,
41
+ 'plugin' => Belt::CLI::PluginCommand,
38
42
  'deploy' => Belt::CLI::DeployCommand,
39
43
  'frontend' => Belt::CLI::FrontendEnvCommand,
40
44
  %w[server s] => Belt::CLI::ServerCommand,
@@ -107,6 +111,7 @@ module Belt
107
111
  deploy frontend <env> Build and deploy frontend to AWS
108
112
  frontend env <env> Write frontend/.env from terraform outputs
109
113
  routes [-g PATTERN] [-f json] Show route definitions
114
+ contracts [-g PATTERN] [-f json] Show API request/response contracts
110
115
  lambda-config [-e ENV] [-f json|terraform] Show merged lambda configuration
111
116
 
112
117
  console Start an interactive console (IRB)
@@ -117,6 +122,7 @@ module Belt
117
122
  setup tables <env> Generate DynamoDB tables from schema
118
123
  setup frontend <env> Generate S3 + CloudFront infrastructure
119
124
  doctor Check system dependencies and AWS config
125
+ plugin new <name> Scaffold a new Belt plugin gem
120
126
  init [environment] <env> terraform init for environment
121
127
  plan [environment] <env> terraform plan for environment
122
128
  apply [environment] <env> terraform apply for environment
@@ -149,6 +155,7 @@ module Belt
149
155
  belt apply wups
150
156
  belt tasks # list all rake tasks
151
157
  belt lambda:build_layer # run a rake task directly
158
+ belt plugin new messaging # scaffold a belt-messaging style plugin gem
152
159
  USAGE
153
160
  end
154
161
  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/root.rb CHANGED
@@ -9,24 +9,32 @@ module Belt
9
9
  @root = path
10
10
  end
11
11
 
12
- # Resolves the path to routes.tf.rb, checking config/ first then infrastructure/ (legacy).
12
+ # Resolves the path to routes.rb, checking config/ first then legacy paths.
13
13
  def self.routes_file
14
14
  candidates = [
15
+ File.join(root, 'config/routes.rb'),
15
16
  File.join(root, 'config/routes.tf.rb'),
16
17
  File.join(root, 'infrastructure/routes.tf.rb')
17
18
  ]
18
19
  candidates.find { |f| File.exist?(f) }
19
20
  end
20
21
 
21
- # Resolves the path to schema.tf.rb, checking config/ first then infrastructure/ (legacy).
22
- def self.schema_file
22
+ # Resolves the path to contracts.rb, checking config/ first then legacy paths.
23
+ def self.contracts_file
23
24
  candidates = [
25
+ File.join(root, 'config/contracts.rb'),
26
+ File.join(root, 'config/contracts.tf.rb'),
24
27
  File.join(root, 'config/schema.tf.rb'),
25
28
  File.join(root, 'infrastructure/schema.tf.rb')
26
29
  ]
27
30
  candidates.find { |f| File.exist?(f) }
28
31
  end
29
32
 
33
+ # Legacy alias for backward compatibility
34
+ def self.schema_file
35
+ contracts_file
36
+ end
37
+
30
38
  # Resolves the lambda config directory.
31
39
  def self.lambda_config_dir
32
40
  File.join(root, 'config/lambda')
@@ -35,6 +43,7 @@ module Belt
35
43
  def self.detect_root
36
44
  dir = Dir.pwd
37
45
  loop do
46
+ return dir if File.exist?(File.join(dir, 'config/routes.rb'))
38
47
  return dir if File.exist?(File.join(dir, 'config/routes.tf.rb'))
39
48
  return dir if File.exist?(File.join(dir, 'infrastructure/routes.tf.rb'))
40
49
 
@@ -5,7 +5,7 @@ require_relative 'inflector'
5
5
  module Belt
6
6
  # DSL for defining API Gateway routes.
7
7
  # Ported from terraform-provider-conveyor-belt/scripts/lib/route_dsl.rb
8
- # so that `belt routes` can parse routes.tf.rb without external dependencies.
8
+ # so that `belt routes` can parse routes.rb without external dependencies.
9
9
 
10
10
  class Route
11
11
  attr_reader :method, :path, :auth, :lambda, :cors, :tables, :route_type,
@@ -518,7 +518,7 @@ module Belt
518
518
  end
519
519
  end
520
520
 
521
- # SchemaBuilder captures request and response model definitions from schema.tf.rb
521
+ # SchemaBuilder captures request and response model definitions from contracts.rb
522
522
  class SchemaBuilder
523
523
  SUPPORTED_TYPES = %i[string number integer boolean array object map list].freeze
524
524
 
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.12'
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
@@ -9,7 +9,8 @@ resource "random_string" "frontend_suffix" {
9
9
 
10
10
  # S3 bucket for frontend static assets
11
11
  resource "aws_s3_bucket" "frontend" {
12
- bucket = "<%= s3_safe_name(@app_name) %>-frontend-${var.environment}-${random_string.frontend_suffix.result}"
12
+ bucket = "<%= s3_safe_name(@app_name) %>-frontend-${var.environment}-${random_string.frontend_suffix.result}"
13
+ force_destroy = true
13
14
 
14
15
  lifecycle {
15
16
  ignore_changes = [bucket]
@@ -9,7 +9,8 @@ resource "random_string" "frontend_suffix" {
9
9
 
10
10
  # S3 bucket for frontend static assets
11
11
  resource "aws_s3_bucket" "frontend" {
12
- bucket = "${var.app_name}-frontend-${var.environment}-${random_string.frontend_suffix.result}"
12
+ bucket = "${var.app_name}-frontend-${var.environment}-${random_string.frontend_suffix.result}"
13
+ force_destroy = true
13
14
 
14
15
  lifecycle {
15
16
  ignore_changes = [bucket]
@@ -22,7 +22,7 @@ resource "conveyor_belt" "main" {
22
22
  provider = conveyor-belt
23
23
 
24
24
  # path.module is infrastructure/modules/app — climb three levels to project root
25
- source = "${path.module}/../../../config/routes.tf.rb"
25
+ source = "${path.module}/../../../config/routes.rb"
26
26
  app_name = var.app_name
27
27
  lambda_source_dir = "${path.module}/../../../lambda"
28
28
  lambda_shared_dirs = ["controllers", "helpers", "lib", "models", "views"]
@@ -6,7 +6,7 @@ This file explains the project structure, tooling, and conventions for AI agents
6
6
 
7
7
  - **Belt** — CLI and runtime framework (like Rails for serverless). Provides Lambda handler, action router, controller base class, and CLI tooling.
8
8
  - **ActiveItem** — ActiveRecord-like ORM for DynamoDB. Models inherit from `ActiveItem::Base`.
9
- - **Conveyor Belt** — Terraform provider that reads a Ruby DSL (`routes.tf.rb`) and creates API Gateway + Lambda + IAM infrastructure.
9
+ - **Conveyor Belt** — Terraform provider that reads a Ruby DSL (`routes.rb`) and creates API Gateway + Lambda + IAM infrastructure.
10
10
  - **Lambda Loadout** — Lambda cold-start optimizer (auto-required by Belt).
11
11
 
12
12
  ## Project Structure
@@ -23,8 +23,8 @@ This file explains the project structure, tooling, and conventions for AI agents
23
23
  │ ├── lib/routes/ # Route manifests (auto-generated)
24
24
  │ └── Gemfile # Lambda-specific dependencies
25
25
  ├── config/
26
- │ ├── routes.tf.rb # API routes (Conveyor Belt DSL)
27
- │ ├── schema.tf.rb # Model schema definitions
26
+ │ ├── routes.rb # API routes (Conveyor Belt DSL)
27
+ │ ├── contracts.rb # API request/response contracts
28
28
  │ └── lambda/ # Per-lambda config (like database.yml)
29
29
  │ └── api.yml # Lambda timeout, memory, env vars
30
30
  ├── infrastructure/
@@ -53,7 +53,7 @@ belt output <env> # terraform output
53
53
 
54
54
  ## How Routing Works
55
55
 
56
- 1. `config/routes.tf.rb` defines routes using a DSL:
56
+ 1. `config/routes.rb` defines routes using a DSL:
57
57
  ```ruby
58
58
  Belt.application.routes.draw do
59
59
  namespace :<%= @app_name %> do
@@ -6,8 +6,8 @@ A serverless application built with [Belt](https://github.com/stowzilla/belt) an
6
6
 
7
7
  ```
8
8
  ├── config/
9
- │ ├── routes.tf.rb # API route definitions
10
- │ ├── schema.tf.rb # Model schema definitions
9
+ │ ├── routes.rb # API route definitions
10
+ │ ├── contracts.rb # API request/response contracts
11
11
  │ └── lambda/ # Per-lambda config (timeout, memory, env vars)
12
12
  │ └── api.yml
13
13
  ├── infrastructure/ # Terraform environments
@@ -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.rb` or `config/contracts.rb` 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 `config/routes.rb` / `config/contracts.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