belt 0.0.6 → 0.1.0

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 (57) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +4 -0
  4. data/exe/belt +6 -0
  5. data/lib/belt/action_router.rb +7 -1
  6. data/lib/belt/cli/app_detection.rb +16 -0
  7. data/lib/belt/cli/bucket_security.rb +122 -0
  8. data/lib/belt/cli/env_resolver.rb +15 -0
  9. data/lib/belt/cli/environment_command.rb +77 -0
  10. data/lib/belt/cli/frontend_command.rb +85 -0
  11. data/lib/belt/cli/frontend_deploy_command.rb +125 -0
  12. data/lib/belt/cli/frontend_setup_command.rb +64 -0
  13. data/lib/belt/cli/generate_command.rb +206 -0
  14. data/lib/belt/cli/new_command.rb +125 -0
  15. data/lib/belt/cli/setup_command.rb +261 -0
  16. data/lib/belt/cli/tables_command.rb +138 -0
  17. data/lib/belt/cli/terraform_command.rb +77 -0
  18. data/lib/belt/cli/views_command.rb +134 -0
  19. data/lib/belt/cli.rb +98 -0
  20. data/lib/belt/lambda_handler.rb +16 -0
  21. data/lib/belt/version.rb +1 -1
  22. data/lib/belt.rb +1 -1
  23. data/lib/templates/environment/backend.tf.erb +8 -0
  24. data/lib/templates/environment/main.tf.erb +42 -0
  25. data/lib/templates/environment/terraform.tfvars.erb +1 -0
  26. data/lib/templates/environment/variables.tf.erb +16 -0
  27. data/lib/templates/frontend/react/index.html.erb +12 -0
  28. data/lib/templates/frontend/react/package.json.erb +20 -0
  29. data/lib/templates/frontend/react/src/App.jsx +14 -0
  30. data/lib/templates/frontend/react/src/index.css +10 -0
  31. data/lib/templates/frontend/react/src/lib/apiClient.js.erb +19 -0
  32. data/lib/templates/frontend/react/src/main.jsx +10 -0
  33. data/lib/templates/frontend/react/src/pages/Home.jsx.erb +10 -0
  34. data/lib/templates/frontend/react/vite.config.js +8 -0
  35. data/lib/templates/frontend_infra/frontend.tf.erb +159 -0
  36. data/lib/templates/generate/controller.rb.erb +59 -0
  37. data/lib/templates/generate/model.rb.erb +20 -0
  38. data/lib/templates/new_app/AGENTS.md.erb +130 -0
  39. data/lib/templates/new_app/Gemfile.erb +6 -0
  40. data/lib/templates/new_app/README.md.erb +25 -0
  41. data/lib/templates/new_app/gitignore.erb +14 -0
  42. data/lib/templates/new_app/infrastructure/routes.tf.rb.erb +5 -0
  43. data/lib/templates/new_app/infrastructure/schema.tf.rb.erb +9 -0
  44. data/lib/templates/new_app/lambda/Gemfile.erb +7 -0
  45. data/lib/templates/new_app/lambda/api.rb.erb +22 -0
  46. data/lib/templates/new_app/lambda/controllers/application_controller.rb.erb +6 -0
  47. data/lib/templates/new_app/lambda/lib/routes/routes.rb.erb +11 -0
  48. data/lib/templates/new_app/lambda/models/application_record.rb.erb +6 -0
  49. data/lib/templates/new_app/lambda/models/concerns/timestampable.rb.erb +23 -0
  50. data/lib/templates/views/Edit.jsx.erb +38 -0
  51. data/lib/templates/views/Form.jsx.erb +34 -0
  52. data/lib/templates/views/Index.jsx.erb +39 -0
  53. data/lib/templates/views/New.jsx.erb +26 -0
  54. data/lib/templates/views/Show.jsx.erb +46 -0
  55. data.tar.gz.sig +0 -0
  56. metadata +51 -3
  57. metadata.gz.sig +0 -0
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'app_detection'
4
+ require_relative 'env_resolver'
5
+
6
+ module Belt
7
+ module CLI
8
+ class TablesCommand
9
+ SCHEMA_FILE = 'infrastructure/schema.tf.rb'
10
+
11
+ include AppDetection
12
+
13
+ def self.run(args)
14
+ env = EnvResolver.resolve(args)
15
+
16
+ if env.nil?
17
+ puts 'Usage: belt setup tables <environment>'
18
+ puts "\nReads schema.tf.rb and generates dynamodb.tf in the environment directory."
19
+ puts 'You can also set BELT_ENV to skip the environment argument.'
20
+ puts "\nExamples:"
21
+ puts ' belt setup tables wups'
22
+ puts ' belt setup tables dev01'
23
+ puts ' BELT_ENV=wups belt setup tables'
24
+ exit 1
25
+ end
26
+
27
+ new(env).run
28
+ end
29
+
30
+ def initialize(env)
31
+ @env = env
32
+ @app_name = detect_app_name
33
+ @env_dir = "infrastructure/#{@env}"
34
+ end
35
+
36
+ def run
37
+ validate!
38
+ models = parse_schema
39
+ if models.empty?
40
+ puts "No models found in #{SCHEMA_FILE}"
41
+ exit 0
42
+ end
43
+
44
+ generate_dynamodb_tf(models)
45
+ end
46
+
47
+ private
48
+
49
+ def validate!
50
+ abort "Error: #{SCHEMA_FILE} not found. Run `belt generate resource` first." unless File.exist?(SCHEMA_FILE)
51
+ return if Dir.exist?(@env_dir)
52
+
53
+ abort "Error: Environment '#{@env}' not found at #{@env_dir}/.\n" \
54
+ "Create it with: belt generate environment #{@env}"
55
+ end
56
+
57
+ def parse_schema
58
+ parser = SchemaParser.new
59
+ schema_content = File.read(SCHEMA_FILE)
60
+
61
+ # Replace DSL wrapper with direct parser call
62
+ # TerraDispatch.schema.draw do ... end → parser.instance_eval do ... end
63
+ inner = schema_content.sub(/\ATerraDispatch\.schema\.draw do\n?/, '').sub(/\nend\s*\z/, '')
64
+ parser.instance_eval(inner, SCHEMA_FILE)
65
+ parser.models
66
+ end
67
+
68
+ def generate_dynamodb_tf(models)
69
+ dest = File.join(@env_dir, 'dynamodb.tf')
70
+ content = render_dynamodb(models)
71
+ File.write(dest, content)
72
+ puts " create #{dest}"
73
+ puts "\n✓ Generated DynamoDB tables for #{models.size} model(s):"
74
+ models.each { |m| puts " • #{table_name(m[:name])}" }
75
+ puts "\nRun `belt apply #{@env}` to create them."
76
+ end
77
+
78
+ def render_dynamodb(models)
79
+ blocks = models.map { |m| render_table(m) }
80
+ "# Auto-generated by Belt from schema.tf.rb\n" \
81
+ "# Do not edit manually — re-run `belt setup tables #{@env}`\n\n#{blocks.join("\n\n")}\n"
82
+ end
83
+
84
+ def render_table(model)
85
+ name = table_name(model[:name])
86
+ <<~HCL
87
+ resource "aws_dynamodb_table" "#{model[:name]}s" {
88
+ name = "#{name}"
89
+ billing_mode = "PAY_PER_REQUEST"
90
+ hash_key = "id"
91
+
92
+ attribute {
93
+ name = "id"
94
+ type = "S"
95
+ }
96
+
97
+ tags = {
98
+ Name = "#{name}"
99
+ Environment = var.environment
100
+ ManagedBy = "Terraform"
101
+ }
102
+ }
103
+ HCL
104
+ end
105
+
106
+ def table_name(model_name)
107
+ "#{@app_name}-#{@env}-#{model_name}s"
108
+ end
109
+
110
+ # Minimal DSL parser for schema.tf.rb
111
+ class SchemaParser
112
+ attr_reader :models
113
+
114
+ def initialize
115
+ @models = []
116
+ end
117
+
118
+ def model(name, &block)
119
+ model_def = ModelParser.new
120
+ model_def.instance_eval(&block) if block
121
+ @models << { name: name.to_s, fields: model_def.fields }
122
+ end
123
+ end
124
+
125
+ class ModelParser
126
+ attr_reader :fields
127
+
128
+ def initialize
129
+ @fields = []
130
+ end
131
+
132
+ def field(name, **opts)
133
+ @fields << { name: name.to_s, type: opts[:type]&.to_s || 'string' }
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'env_resolver'
4
+
5
+ module Belt
6
+ module CLI
7
+ class TerraformCommand
8
+ ACTIONS = %w[init plan apply destroy output].freeze
9
+
10
+ def self.run(action, args)
11
+ env = EnvResolver.resolve(args)
12
+
13
+ if env.nil?
14
+ puts "Usage: belt #{action} <environment> [terraform flags...]"
15
+ puts "\nYou can also set BELT_ENV to skip the environment argument."
16
+ puts "\nExamples:"
17
+ puts " belt #{action} wups"
18
+ puts " belt #{action} dev01"
19
+ puts " BELT_ENV=wups belt #{action}"
20
+ puts ' belt plan staging -target=module.lambda'
21
+ puts "\nAvailable environments:"
22
+ list_environments.each { |e| puts " #{e}" }
23
+ exit 1
24
+ end
25
+
26
+ new(action, env, args).run
27
+ end
28
+
29
+ def self.list_environments
30
+ infra_dir = find_infrastructure_dir
31
+ return [] unless infra_dir
32
+
33
+ Dir.children(infra_dir)
34
+ .select { |d| File.directory?(File.join(infra_dir, d)) }
35
+ .reject { |d| d.start_with?('.') || d == 'modules' }
36
+ .sort
37
+ end
38
+
39
+ def self.find_infrastructure_dir
40
+ %w[infrastructure infra].each do |dir|
41
+ return dir if Dir.exist?(dir)
42
+ end
43
+ nil
44
+ end
45
+
46
+ def initialize(action, env, extra_args)
47
+ @action = action
48
+ @env = env
49
+ @extra_args = extra_args
50
+ @infra_dir = self.class.find_infrastructure_dir
51
+ end
52
+
53
+ def run
54
+ validate!
55
+ env_dir = File.join(@infra_dir, @env)
56
+ args = ['terraform', @action, *@extra_args]
57
+ puts "belt → #{args.join(' ')} (in #{env_dir}/)"
58
+ Dir.chdir(env_dir) { exec(*args) }
59
+ end
60
+
61
+ private
62
+
63
+ def validate!
64
+ unless @infra_dir
65
+ abort "Error: No infrastructure/ directory found. Run `belt generate environment #{@env}` first."
66
+ end
67
+
68
+ env_dir = File.join(@infra_dir, @env)
69
+ return if Dir.exist?(env_dir)
70
+
71
+ abort "Error: Environment '#{@env}' not found at #{env_dir}/.\n\n" \
72
+ "Available environments:\n#{self.class.list_environments.map { |e| " #{e}" }.join("\n")}\n\n" \
73
+ "Create it with: belt generate environment #{@env}"
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'erb'
5
+
6
+ module Belt
7
+ module CLI
8
+ class ViewsCommand
9
+ TEMPLATE_DIR = File.expand_path('../../templates/views', __dir__)
10
+
11
+ def self.run(args)
12
+ name = args.shift
13
+ if name.nil? || name.empty?
14
+ puts 'Usage: belt generate views <resource> [field:type ...]'
15
+ puts "\nGenerates React pages for all REST actions (index, show, new, edit)."
16
+ puts "\nExamples:"
17
+ puts ' belt generate views post title:string content:text status:string'
18
+ puts ' belt generate views comment body:text author:string'
19
+ exit 1
20
+ end
21
+
22
+ fields = args.map do |arg|
23
+ n, t = arg.split(':', 2)
24
+ { name: n, type: t || 'string' }
25
+ end
26
+
27
+ # If no fields provided, try to read from schema.tf.rb
28
+ fields = read_schema_fields(name) if fields.empty?
29
+
30
+ new(name, fields).generate
31
+ end
32
+
33
+ def self.read_schema_fields(name)
34
+ schema_file = 'infrastructure/schema.tf.rb'
35
+ return [] unless File.exist?(schema_file)
36
+
37
+ content = File.read(schema_file)
38
+ singular = name.end_with?('s') ? name.chomp('s') : name
39
+
40
+ # Extract fields from model block
41
+ if content =~ /model :#{singular} do\n(.*?)\n\s*end/m
42
+ ::Regexp.last_match(1).scan(/field :(\w+), type: :(\w+)/).except('created_at', 'updated_at')
43
+ .map do |n, t|
44
+ {
45
+ name: n, type: t
46
+ }
47
+ end
48
+ else
49
+ []
50
+ end
51
+ end
52
+
53
+ def initialize(name, fields)
54
+ @name = name.downcase.gsub(/[^a-z0-9_]/, '_')
55
+ @fields = fields
56
+ @resource_name = @name.end_with?('s') ? @name : "#{@name}s"
57
+ @singular_name = @name.end_with?('s') ? @name.chomp('s') : @name
58
+ @class_name = @singular_name.split('_').map(&:capitalize).join
59
+ end
60
+
61
+ def generate
62
+ unless Dir.exist?('frontend/src')
63
+ puts '✗ No frontend/ directory found. Run `belt generate frontend react` first.'
64
+ exit 1
65
+ end
66
+
67
+ pages_dir = "frontend/src/pages/#{@resource_name}"
68
+ FileUtils.mkdir_p(pages_dir)
69
+
70
+ write_template('Index.jsx.erb', "#{pages_dir}/#{@class_name}sIndex.jsx")
71
+ write_template('Show.jsx.erb', "#{pages_dir}/#{@class_name}Show.jsx")
72
+ write_template('New.jsx.erb', "#{pages_dir}/#{@class_name}New.jsx")
73
+ write_template('Edit.jsx.erb', "#{pages_dir}/#{@class_name}Edit.jsx")
74
+ write_template('Form.jsx.erb', "#{pages_dir}/#{@class_name}Form.jsx")
75
+
76
+ inject_routes
77
+
78
+ puts "\n✓ Views for '#{@singular_name}' generated!"
79
+ puts "\nFiles created:"
80
+ puts " #{pages_dir}/#{@class_name}sIndex.jsx"
81
+ puts " #{pages_dir}/#{@class_name}Show.jsx"
82
+ puts " #{pages_dir}/#{@class_name}New.jsx"
83
+ puts " #{pages_dir}/#{@class_name}Edit.jsx"
84
+ puts " #{pages_dir}/#{@class_name}Form.jsx"
85
+ puts ' frontend/src/App.jsx (updated)'
86
+ end
87
+
88
+ private
89
+
90
+ def write_template(template_name, dest_path)
91
+ template_path = File.join(TEMPLATE_DIR, template_name)
92
+ content = ERB.new(File.read(template_path), trim_mode: '-').result(binding)
93
+ File.write(dest_path, content)
94
+ puts " create #{dest_path}"
95
+ end
96
+
97
+ def inject_routes
98
+ app_jsx = 'frontend/src/App.jsx'
99
+ return unless File.exist?(app_jsx)
100
+
101
+ content = File.read(app_jsx)
102
+ pages_dir = @resource_name
103
+
104
+ import_lines = [
105
+ "import #{@class_name}sIndex from './pages/#{pages_dir}/#{@class_name}sIndex'",
106
+ "import #{@class_name}Show from './pages/#{pages_dir}/#{@class_name}Show'",
107
+ "import #{@class_name}New from './pages/#{pages_dir}/#{@class_name}New'",
108
+ "import #{@class_name}Edit from './pages/#{pages_dir}/#{@class_name}Edit'"
109
+ ]
110
+
111
+ route_lines = [
112
+ " <Route path=\"/#{@resource_name}\" element={<#{@class_name}sIndex />} />",
113
+ " <Route path=\"/#{@resource_name}/new\" element={<#{@class_name}New />} />",
114
+ " <Route path=\"/#{@resource_name}/:id\" element={<#{@class_name}Show />} />",
115
+ " <Route path=\"/#{@resource_name}/:id/edit\" element={<#{@class_name}Edit />} />"
116
+ ]
117
+
118
+ # Add imports after last import line
119
+ last_import_idx = content.rindex(/^import .+$/)
120
+ if last_import_idx
121
+ end_of_line = content.index("\n", last_import_idx)
122
+ content.insert(end_of_line, "\n#{import_lines.join("\n")}")
123
+ end
124
+
125
+ # Add routes before closing </Routes> (no regex — avoids polynomial backtracking)
126
+ close_idx = content.index('</Routes>')
127
+ content.insert(close_idx, "#{route_lines.join("\n")}\n") if close_idx
128
+
129
+ File.write(app_jsx, content)
130
+ puts " update #{app_jsx}"
131
+ end
132
+ end
133
+ end
134
+ end
data/lib/belt/cli.rb ADDED
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'version'
4
+ require_relative 'cli/env_resolver'
5
+ require_relative 'cli/new_command'
6
+ require_relative 'cli/generate_command'
7
+ require_relative 'cli/frontend_command'
8
+ require_relative 'cli/frontend_setup_command'
9
+ require_relative 'cli/frontend_deploy_command'
10
+ require_relative 'cli/views_command'
11
+ require_relative 'cli/setup_command'
12
+ require_relative 'cli/terraform_command'
13
+
14
+ module Belt
15
+ module CLI
16
+ COMMANDS = {
17
+ 'new' => Belt::CLI::NewCommand,
18
+ 'generate' => Belt::CLI::GenerateCommand,
19
+ 'g' => Belt::CLI::GenerateCommand,
20
+ 'setup' => Belt::CLI::SetupCommand,
21
+ 'deploy' => lambda { |args|
22
+ subcommand = args.shift
23
+ if subcommand == 'frontend'
24
+ Belt::CLI::FrontendDeployCommand.run(args)
25
+ else
26
+ puts 'Usage: belt deploy frontend <environment>'
27
+ exit 1
28
+ end
29
+ },
30
+ '--version' => ->(_args) { puts "Belt #{Belt::VERSION}" },
31
+ '-v' => ->(_args) { puts "Belt #{Belt::VERSION}" }
32
+ }.freeze
33
+
34
+ TERRAFORM_ACTIONS = Belt::CLI::TerraformCommand::ACTIONS
35
+
36
+ def self.start(args)
37
+ command = args.shift
38
+
39
+ if command.nil?
40
+ puts usage
41
+ exit 1
42
+ end
43
+
44
+ # Terraform shorthand: belt init wups, belt plan wups, belt apply wups
45
+ return Belt::CLI::TerraformCommand.run(command, args) if TERRAFORM_ACTIONS.include?(command)
46
+
47
+ handler = COMMANDS[command]
48
+
49
+ if handler.nil?
50
+ puts "Unknown command: #{command}\n\n#{usage}"
51
+ exit 1
52
+ end
53
+
54
+ if handler.is_a?(Proc)
55
+ handler.call(args)
56
+ else
57
+ handler.run(args)
58
+ end
59
+ end
60
+
61
+ def self.usage
62
+ <<~USAGE
63
+ Usage: belt <command> [options]
64
+
65
+ Commands:
66
+ new <app_name> [--frontend react] Create a new Belt application
67
+ generate <resource|model|controller> <name> Generate components
68
+ generate frontend <react|vue|svelte> Scaffold a frontend app
69
+ generate views <resource> [fields...] Generate React pages for REST actions
70
+ generate environment <name> Create a new environment
71
+ setup state Create/select S3 state bucket
72
+ setup tables <env> Generate DynamoDB tables from schema
73
+ setup frontend <env> Generate S3 + CloudFront infrastructure
74
+ deploy frontend <env> Build and deploy frontend to AWS
75
+ init <env> terraform init for environment
76
+ plan <env> terraform plan for environment
77
+ apply <env> terraform apply for environment
78
+ destroy <env> terraform destroy for environment
79
+ output <env> terraform output for environment
80
+ --version Show Belt version
81
+
82
+ Environment:
83
+ Set BELT_ENV to skip the <env> argument:
84
+ export BELT_ENV=wups
85
+ belt apply # uses BELT_ENV
86
+ belt apply dev01 # explicit arg wins
87
+
88
+ Examples:
89
+ belt new blog --frontend react
90
+ belt generate resource post title:string content:text status:string
91
+ belt generate frontend react
92
+ belt setup frontend wups
93
+ belt deploy frontend wups
94
+ belt apply wups
95
+ USAGE
96
+ end
97
+ end
98
+ end
@@ -25,6 +25,22 @@ module Belt
25
25
 
26
26
  def self.included(base)
27
27
  base.instance_variable_set(:@belt_lambda_handler_included, true)
28
+
29
+ # Skip auto-registration in test environments to avoid stale paths
30
+ return if ENV['BELT_ENV'] == 'test' || ENV['RACK_ENV'] == 'test' || defined?(RSpec)
31
+
32
+ # Auto-register controllers directory relative to the including file's location
33
+ caller_file = caller_locations(1, 1)&.first&.path
34
+ return unless caller_file
35
+
36
+ controllers_dir = File.join(File.dirname(caller_file), 'controllers')
37
+ return unless File.directory?(controllers_dir)
38
+
39
+ Belt.controller_paths << controllers_dir
40
+ Dir.children(controllers_dir).each do |child|
41
+ subdir = File.join(controllers_dir, child)
42
+ Belt.controller_paths << subdir if File.directory?(subdir)
43
+ end
28
44
  end
29
45
 
30
46
  # API Gateway Lambda handler.
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.0.6'
4
+ VERSION = '0.1.0'
5
5
  end
data/lib/belt.rb CHANGED
@@ -45,7 +45,7 @@ module Belt
45
45
  private
46
46
 
47
47
  def discover_gem_paths(subdir)
48
- Gem::Specification.each.filter_map do |spec|
48
+ Gem.loaded_specs.each_value.filter_map do |spec|
49
49
  path = File.join(spec.gem_dir, subdir)
50
50
  path if File.directory?(path)
51
51
  end
@@ -0,0 +1,8 @@
1
+ terraform {
2
+ backend "s3" {
3
+ bucket = "<%= @app_name %>-terraform-state"
4
+ key = "<%= @env_name %>/terraform.tfstate"
5
+ region = "us-east-1"
6
+ encrypt = true
7
+ }
8
+ }
@@ -0,0 +1,42 @@
1
+ terraform {
2
+ required_providers {
3
+ aws = {
4
+ source = "hashicorp/aws"
5
+ version = "~> 5.0"
6
+ }
7
+ random = {
8
+ source = "hashicorp/random"
9
+ version = "~> 3.0"
10
+ }
11
+ dispatcher = {
12
+ source = "terraform.local/stowzilla/dispatcher"
13
+ version = "99.0.0"
14
+ }
15
+ }
16
+ }
17
+
18
+ provider "aws" {
19
+ region = var.aws_region
20
+
21
+ default_tags {
22
+ tags = {
23
+ Project = "<%= @app_name.capitalize %>"
24
+ Environment = var.environment
25
+ ManagedBy = "Terraform"
26
+ }
27
+ }
28
+ }
29
+
30
+ provider "dispatcher" {
31
+ environment = var.environment
32
+ aws_region = var.aws_region
33
+ }
34
+
35
+ resource "dispatcher" "main" {
36
+ source = "${path.module}/../routes.tf.rb"
37
+ app_name = var.app_name
38
+ lambda_source_dir = "${path.module}/../../lambda"
39
+ lambda_shared_dirs = ["controllers", "helpers", "lib", "models", "templates"]
40
+ frontend_urls = ["http://localhost:3000"]
41
+ friendly_errors = true
42
+ }
@@ -0,0 +1 @@
1
+ environment = "<%= @env_name %>"
@@ -0,0 +1,16 @@
1
+ variable "app_name" {
2
+ description = "Name of the application"
3
+ type = string
4
+ default = "<%= @app_name %>"
5
+ }
6
+
7
+ variable "environment" {
8
+ description = "Environment name"
9
+ type = string
10
+ }
11
+
12
+ variable "aws_region" {
13
+ description = "AWS region"
14
+ type = string
15
+ default = "us-east-1"
16
+ }
@@ -0,0 +1,12 @@
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><%= @module_name %></title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.jsx"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "<%= @app_name %>",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.3.1",
13
+ "react-dom": "^18.3.1",
14
+ "react-router-dom": "^6.28.0"
15
+ },
16
+ "devDependencies": {
17
+ "@vitejs/plugin-react": "^4.3.4",
18
+ "vite": "^6.0.0"
19
+ }
20
+ }
@@ -0,0 +1,14 @@
1
+ import { BrowserRouter, Routes, Route } from 'react-router-dom'
2
+ import Home from './pages/Home'
3
+
4
+ function App() {
5
+ return (
6
+ <BrowserRouter>
7
+ <Routes>
8
+ <Route path="/" element={<Home />} />
9
+ </Routes>
10
+ </BrowserRouter>
11
+ )
12
+ }
13
+
14
+ export default App
@@ -0,0 +1,10 @@
1
+ :root {
2
+ font-family: system-ui, -apple-system, sans-serif;
3
+ line-height: 1.5;
4
+ color: #213547;
5
+ background-color: #ffffff;
6
+ }
7
+
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+
10
+ body { min-height: 100vh; }
@@ -0,0 +1,19 @@
1
+ const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000'
2
+
3
+ export async function apiClient(path, options = {}) {
4
+ const { method = 'GET', body, headers = {} } = options
5
+
6
+ const config = {
7
+ method,
8
+ headers: { 'Content-Type': 'application/json', ...headers }
9
+ }
10
+
11
+ if (body) config.body = JSON.stringify(body)
12
+
13
+ const response = await fetch(`${API_URL}${path}`, config)
14
+ const data = await response.json()
15
+
16
+ if (!response.ok) throw new Error(data.error || `Request failed: ${response.status}`)
17
+
18
+ return data
19
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App'
4
+ import './index.css'
5
+
6
+ ReactDOM.createRoot(document.getElementById('root')).render(
7
+ <React.StrictMode>
8
+ <App />
9
+ </React.StrictMode>
10
+ )
@@ -0,0 +1,10 @@
1
+ function Home() {
2
+ 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>
7
+ )
8
+ }
9
+
10
+ export default Home
@@ -0,0 +1,8 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: { port: 3000 },
7
+ build: { outDir: 'dist' }
8
+ })