belt 0.1.4 → 0.1.6
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 +4 -4
- data/lib/belt/action_router.rb +7 -0
- data/lib/belt/assets/belt-default.jpg +0 -0
- data/lib/belt/assets/welcome.css +112 -0
- data/lib/belt/cli/app_detection.rb +17 -1
- data/lib/belt/cli/deploy_command.rb +495 -0
- data/lib/belt/cli/environment_command.rb +12 -7
- data/lib/belt/cli/generate_command.rb +165 -10
- data/lib/belt/cli/new_command.rb +168 -25
- data/lib/belt/cli/server_command.rb +302 -0
- data/lib/belt/cli/setup_command.rb +40 -6
- data/lib/belt/cli.rb +12 -11
- data/lib/belt/controllers/welcome_controller.rb +46 -0
- data/lib/belt/rendering.rb +136 -0
- data/lib/belt/version.rb +1 -1
- data/lib/belt/views/welcome/show.html.erb +51 -0
- data/lib/belt.rb +2 -0
- data/lib/belt_controller/base.rb +2 -0
- data/lib/templates/environment/main.tf.erb +17 -3
- data/lib/templates/environment/outputs.tf.erb +9 -0
- data/lib/templates/new_app/infrastructure/routes.tf.rb.erb +2 -1
- data/lib/templates/new_app/lambda/api.rb.erb +2 -2
- data/lib/templates/new_app/lambda/controllers/application_controller.rb.erb +1 -1
- data/lib/templates/new_app/lambda/lib/routes/routes.rb.erb +2 -1
- metadata +9 -1
|
@@ -26,9 +26,10 @@ module Belt
|
|
|
26
26
|
new(env_name).generate
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def initialize(env_name)
|
|
29
|
+
def initialize(env_name, quiet: false)
|
|
30
30
|
@env_name = env_name.downcase.gsub(/[^a-z0-9_-]/, '')
|
|
31
31
|
@app_name = detect_app_name
|
|
32
|
+
@quiet = quiet
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
def generate
|
|
@@ -49,11 +50,14 @@ module Belt
|
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
puts "\n✓ Environment '#{@env_name}' created!"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
|
|
54
|
+
unless @quiet
|
|
55
|
+
puts "\nNext steps:"
|
|
56
|
+
puts " cd #{dest_dir}"
|
|
57
|
+
puts ' terraform init'
|
|
58
|
+
puts ' terraform plan'
|
|
59
|
+
puts ' terraform apply'
|
|
60
|
+
end
|
|
57
61
|
end
|
|
58
62
|
|
|
59
63
|
private
|
|
@@ -63,7 +67,8 @@ module Belt
|
|
|
63
67
|
'main.tf.erb' => 'main.tf',
|
|
64
68
|
'backend.tf.erb' => 'backend.tf',
|
|
65
69
|
'variables.tf.erb' => 'variables.tf',
|
|
66
|
-
'terraform.tfvars.erb' => 'terraform.tfvars'
|
|
70
|
+
'terraform.tfvars.erb' => 'terraform.tfvars',
|
|
71
|
+
'outputs.tf.erb' => 'outputs.tf'
|
|
67
72
|
}
|
|
68
73
|
end
|
|
69
74
|
|
|
@@ -15,19 +15,79 @@ module Belt
|
|
|
15
15
|
|
|
16
16
|
include AppDetection
|
|
17
17
|
|
|
18
|
+
FIELD_TYPES = %w[string text integer float boolean date datetime].freeze
|
|
19
|
+
|
|
20
|
+
GENERATOR_HELP = {
|
|
21
|
+
'resource' => {
|
|
22
|
+
description: 'Generate a model, controller, routes, and schema for a REST resource.',
|
|
23
|
+
usage: 'belt generate resource <name> [field:type ...] [options]',
|
|
24
|
+
options: [
|
|
25
|
+
['--skip-views', 'Skip generating frontend view pages']
|
|
26
|
+
],
|
|
27
|
+
examples: [
|
|
28
|
+
['belt g resource post title:string body:text', 'Post with title and body fields'],
|
|
29
|
+
['belt g resource comment author:string body:text status:string', 'Comment with multiple fields'],
|
|
30
|
+
['belt g resource task --skip-views', 'Task resource without frontend pages']
|
|
31
|
+
],
|
|
32
|
+
notes: <<~NOTES
|
|
33
|
+
Creates:
|
|
34
|
+
lambda/models/<name>.rb Model with validations and fields
|
|
35
|
+
lambda/controllers/<app>/<names>_controller.rb RESTful controller (index, show, create, update, destroy)
|
|
36
|
+
infrastructure/routes.tf.rb Route entry added
|
|
37
|
+
infrastructure/schema.tf.rb DynamoDB table schema added
|
|
38
|
+
lambda/lib/routes/<app>_routes.rb Route manifest updated
|
|
39
|
+
NOTES
|
|
40
|
+
},
|
|
41
|
+
'model' => {
|
|
42
|
+
description: 'Generate an ActiveItem model with validations and DynamoDB field definitions.',
|
|
43
|
+
usage: 'belt generate model <name> [field:type ...]',
|
|
44
|
+
options: [],
|
|
45
|
+
examples: [
|
|
46
|
+
['belt g model user email:string name:string', 'User model with email and name'],
|
|
47
|
+
['belt g model event title:string starts_at:datetime', 'Event model with datetime field']
|
|
48
|
+
],
|
|
49
|
+
notes: <<~NOTES
|
|
50
|
+
Creates:
|
|
51
|
+
lambda/models/<name>.rb Model class inheriting from ActiveItem::Base
|
|
52
|
+
NOTES
|
|
53
|
+
},
|
|
54
|
+
'controller' => {
|
|
55
|
+
description: 'Generate a controller with standard REST actions.',
|
|
56
|
+
usage: 'belt generate controller <name>',
|
|
57
|
+
options: [],
|
|
58
|
+
examples: [
|
|
59
|
+
['belt g controller posts', 'Posts controller with CRUD actions'],
|
|
60
|
+
['belt g controller admin/users', 'Namespaced controller']
|
|
61
|
+
],
|
|
62
|
+
notes: <<~NOTES
|
|
63
|
+
Creates:
|
|
64
|
+
lambda/controllers/<app>/<name>_controller.rb Controller class
|
|
65
|
+
NOTES
|
|
66
|
+
}
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
18
69
|
def self.run(args)
|
|
19
70
|
generator = args.shift
|
|
20
71
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
72
|
+
# Top-level help: belt generate --help or belt generate (no args)
|
|
73
|
+
if generator.nil? || generator =~ /\A-/
|
|
74
|
+
print_generate_help
|
|
75
|
+
exit 0
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
unless GENERATORS.include?(generator)
|
|
79
|
+
puts "Unknown generator: '#{generator}'"
|
|
80
|
+
puts "Available generators: #{GENERATORS.join(', ')}"
|
|
81
|
+
puts "\nRun 'belt generate --help' for usage information."
|
|
28
82
|
exit 1
|
|
29
83
|
end
|
|
30
84
|
|
|
85
|
+
# Per-generator help: belt g resource --help
|
|
86
|
+
if args.include?('--help') || args.include?('-h')
|
|
87
|
+
print_generator_help(generator)
|
|
88
|
+
exit 0
|
|
89
|
+
end
|
|
90
|
+
|
|
31
91
|
return Belt::CLI::EnvironmentCommand.run(args) if generator == 'environment'
|
|
32
92
|
|
|
33
93
|
return Belt::CLI::FrontendCommand.run(args) if generator == 'frontend'
|
|
@@ -36,10 +96,12 @@ module Belt
|
|
|
36
96
|
|
|
37
97
|
name = args.shift
|
|
38
98
|
if name.nil? || name.empty?
|
|
39
|
-
|
|
40
|
-
exit
|
|
99
|
+
print_generator_help(generator)
|
|
100
|
+
exit 0
|
|
41
101
|
end
|
|
42
102
|
|
|
103
|
+
validate_resource_name!(name, generator)
|
|
104
|
+
|
|
43
105
|
skip_views = args.delete('--skip-views')
|
|
44
106
|
fields = args.map { |arg| parse_field(arg) }
|
|
45
107
|
new(generator, name, fields, skip_views: skip_views).generate
|
|
@@ -50,12 +112,105 @@ module Belt
|
|
|
50
112
|
{ name: name, type: type || 'string' }
|
|
51
113
|
end
|
|
52
114
|
|
|
115
|
+
RESERVED_NAMES = %w[
|
|
116
|
+
help new edit create update destroy index show delete remove
|
|
117
|
+
application base controller model resource generate
|
|
118
|
+
belt setup environment frontend views routes
|
|
119
|
+
].freeze
|
|
120
|
+
|
|
121
|
+
def self.validate_resource_name!(name, generator)
|
|
122
|
+
errors = []
|
|
123
|
+
|
|
124
|
+
errors << 'must start with a letter' unless name.match?(/\A[a-zA-Z]/)
|
|
125
|
+
errors << 'must be at least 2 characters' if name.length < 2
|
|
126
|
+
errors << 'must only contain letters, numbers, and underscores' unless name.match?(/\A[a-zA-Z][a-zA-Z0-9_]*\z/)
|
|
127
|
+
errors << 'must not start or end with an underscore' if name.match?(/\A_|_\z/)
|
|
128
|
+
errors << 'must not contain consecutive underscores' if name.include?('__')
|
|
129
|
+
errors << "is a reserved name" if RESERVED_NAMES.include?(name.downcase.chomp('s'))
|
|
130
|
+
errors << "is a reserved name" if RESERVED_NAMES.include?(name.downcase)
|
|
131
|
+
|
|
132
|
+
return if errors.empty?
|
|
133
|
+
|
|
134
|
+
puts "\n✗ Invalid #{generator} name '#{name}':"
|
|
135
|
+
errors.uniq.each { |e| puts " - #{e}" }
|
|
136
|
+
puts "\nName must start with a letter, be at least 2 characters, and contain only letters, numbers, and single underscores."
|
|
137
|
+
exit 1
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def self.print_generate_help
|
|
141
|
+
puts <<~HELP
|
|
142
|
+
Usage: belt generate <generator> <name> [field:type ...] [options]
|
|
143
|
+
belt g <generator> <name> [field:type ...] [options]
|
|
144
|
+
|
|
145
|
+
Generators:
|
|
146
|
+
resource Generate model, controller, routes, and schema (full REST resource)
|
|
147
|
+
model Generate an ActiveItem model
|
|
148
|
+
controller Generate a controller
|
|
149
|
+
environment Create a new deployment environment
|
|
150
|
+
frontend Scaffold a frontend app (react, vue, svelte)
|
|
151
|
+
views Generate React pages for a resource
|
|
152
|
+
|
|
153
|
+
Field Types:
|
|
154
|
+
#{FIELD_TYPES.join(', ')}
|
|
155
|
+
|
|
156
|
+
Examples:
|
|
157
|
+
belt g resource post title:string body:text status:string
|
|
158
|
+
belt g model user email:string name:string
|
|
159
|
+
belt g controller comments
|
|
160
|
+
belt g environment staging
|
|
161
|
+
belt g frontend react
|
|
162
|
+
belt g views post title:string body:text
|
|
163
|
+
|
|
164
|
+
Run 'belt generate <generator> --help' for detailed help on a specific generator.
|
|
165
|
+
HELP
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def self.print_generator_help(generator)
|
|
169
|
+
info = GENERATOR_HELP[generator]
|
|
170
|
+
|
|
171
|
+
unless info
|
|
172
|
+
puts "Usage: belt generate #{generator} <name>"
|
|
173
|
+
puts "\nRun 'belt generate #{generator} --help' is not yet documented."
|
|
174
|
+
puts "Try 'belt generate --help' for the full list of generators."
|
|
175
|
+
return
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
puts info[:description]
|
|
179
|
+
puts
|
|
180
|
+
puts "Usage: #{info[:usage]}"
|
|
181
|
+
|
|
182
|
+
if info[:options].any?
|
|
183
|
+
puts "\nOptions:"
|
|
184
|
+
info[:options].each do |flag, desc|
|
|
185
|
+
puts " %-20s %s" % [flag, desc]
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
puts "\nField Types:"
|
|
190
|
+
puts " #{FIELD_TYPES.join(', ')}"
|
|
191
|
+
|
|
192
|
+
puts "\nExamples:"
|
|
193
|
+
info[:examples].each do |cmd, desc|
|
|
194
|
+
puts " #{cmd}"
|
|
195
|
+
puts " #{desc}" if desc
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
puts "\n#{info[:notes]}" if info[:notes]
|
|
199
|
+
|
|
200
|
+
puts "Naming Rules:"
|
|
201
|
+
puts " - Must start with a letter"
|
|
202
|
+
puts " - At least 2 characters"
|
|
203
|
+
puts " - Letters, numbers, and single underscores only"
|
|
204
|
+
puts " - No leading/trailing/consecutive underscores"
|
|
205
|
+
puts " - Cannot use reserved names (help, new, create, destroy, etc.)"
|
|
206
|
+
end
|
|
207
|
+
|
|
53
208
|
def initialize(generator, name, fields, skip_views: false)
|
|
54
209
|
@generator = generator
|
|
55
210
|
@name = name.downcase.gsub(/[^a-z0-9_]/, '_')
|
|
56
211
|
@fields = fields
|
|
57
212
|
@skip_views = skip_views
|
|
58
|
-
@app_name =
|
|
213
|
+
@app_name = detect_namespace
|
|
59
214
|
@module_name = @app_name.split(/[-_]/).map(&:capitalize).join
|
|
60
215
|
@resource_name = @name.end_with?('s') ? @name : "#{@name}s"
|
|
61
216
|
@singular_name = @name.end_with?('s') ? @name.chomp('s') : @name
|
data/lib/belt/cli/new_command.rb
CHANGED
|
@@ -2,40 +2,76 @@
|
|
|
2
2
|
|
|
3
3
|
require 'fileutils'
|
|
4
4
|
require 'erb'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'open3'
|
|
7
|
+
require_relative 'setup_command'
|
|
5
8
|
|
|
6
9
|
module Belt
|
|
7
10
|
module CLI
|
|
8
11
|
class NewCommand
|
|
9
12
|
TEMPLATE_DIR = File.expand_path('../../templates/new_app', __dir__)
|
|
13
|
+
DEFAULT_ENVIRONMENTS = %w[dev prod].freeze
|
|
10
14
|
|
|
11
15
|
def self.run(args)
|
|
12
16
|
app_name = nil
|
|
13
17
|
frontend = nil
|
|
18
|
+
bucket = nil
|
|
19
|
+
environments = nil
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
i = 0
|
|
22
|
+
while i < args.length
|
|
23
|
+
arg = args[i]
|
|
24
|
+
case arg
|
|
25
|
+
when '--frontend'
|
|
26
|
+
if arg.include?('=')
|
|
27
|
+
frontend = arg.split('=', 2).last
|
|
28
|
+
else
|
|
29
|
+
i += 1
|
|
30
|
+
frontend = args[i]
|
|
31
|
+
end
|
|
32
|
+
when /^--frontend=/
|
|
33
|
+
frontend = arg.split('=', 2).last
|
|
34
|
+
when '--bucket', '--state-bucket'
|
|
35
|
+
i += 1
|
|
36
|
+
bucket = args[i]
|
|
37
|
+
when /^--bucket=/
|
|
38
|
+
bucket = arg.split('=', 2).last
|
|
39
|
+
when /^--state-bucket=/
|
|
40
|
+
bucket = arg.split('=', 2).last
|
|
41
|
+
when '--environments'
|
|
42
|
+
i += 1
|
|
43
|
+
environments = args[i]
|
|
44
|
+
when /^--environments=/
|
|
45
|
+
environments = arg.split('=', 2).last
|
|
46
|
+
else
|
|
47
|
+
app_name ||= arg unless arg.start_with?('-')
|
|
24
48
|
end
|
|
49
|
+
i += 1
|
|
25
50
|
end
|
|
26
51
|
|
|
27
52
|
if app_name.nil? || app_name.empty?
|
|
28
|
-
puts 'Usage: belt new <app_name> [
|
|
53
|
+
puts 'Usage: belt new <app_name> [options]'
|
|
54
|
+
puts ''
|
|
55
|
+
puts 'Options:'
|
|
56
|
+
puts ' --frontend react|vue|svelte Set up frontend framework'
|
|
57
|
+
puts ' --bucket BUCKET_NAME S3 bucket for Terraform state'
|
|
58
|
+
puts ' --state-bucket BUCKET_NAME Alias for --bucket'
|
|
59
|
+
puts ' --environments dev,prod Comma-separated environments (default: dev,prod)'
|
|
60
|
+
puts ' Use "none" to skip environment setup'
|
|
29
61
|
exit 1
|
|
30
62
|
end
|
|
31
63
|
|
|
32
|
-
new(app_name, frontend: frontend).generate
|
|
64
|
+
new(app_name, frontend: frontend, bucket: bucket, environments: environments).generate
|
|
33
65
|
end
|
|
34
66
|
|
|
35
|
-
def initialize(app_name, frontend: nil)
|
|
67
|
+
def initialize(app_name, frontend: nil, bucket: nil, environments: nil)
|
|
36
68
|
@app_name = app_name.gsub(/[^a-z0-9_-]/i, '_').downcase
|
|
37
69
|
@module_name = @app_name.split(/[-_]/).map(&:capitalize).join
|
|
38
70
|
@frontend = frontend
|
|
71
|
+
@bucket = bucket
|
|
72
|
+
@environments = parse_environments(environments)
|
|
73
|
+
@resolved_bucket = nil
|
|
74
|
+
@state_setup_succeeded = false
|
|
39
75
|
end
|
|
40
76
|
|
|
41
77
|
def generate
|
|
@@ -47,19 +83,23 @@ module Belt
|
|
|
47
83
|
puts "Creating new Belt application: #{@app_name}"
|
|
48
84
|
create_structure
|
|
49
85
|
generate_frontend if @frontend
|
|
86
|
+
generate_environments
|
|
50
87
|
init_git
|
|
88
|
+
run_bundle_install
|
|
89
|
+
setup_state
|
|
51
90
|
puts "\n✓ #{@app_name} created successfully!"
|
|
52
|
-
|
|
53
|
-
puts " cd #{@app_name}"
|
|
54
|
-
puts ' bundle install'
|
|
55
|
-
puts ' cd frontend && npm install && npm run dev' if @frontend
|
|
56
|
-
puts ' # Define your models in infrastructure/schema.tf.rb'
|
|
57
|
-
puts ' # Define your routes in infrastructure/routes.tf.rb'
|
|
58
|
-
puts " # Add controllers in lambda/controllers/#{@app_name}/"
|
|
91
|
+
print_next_steps
|
|
59
92
|
end
|
|
60
93
|
|
|
61
94
|
private
|
|
62
95
|
|
|
96
|
+
def parse_environments(env_string)
|
|
97
|
+
return DEFAULT_ENVIRONMENTS if env_string.nil? || env_string.empty?
|
|
98
|
+
return [] if env_string.downcase == 'none'
|
|
99
|
+
|
|
100
|
+
env_string.split(',').map(&:strip).reject(&:empty?)
|
|
101
|
+
end
|
|
102
|
+
|
|
63
103
|
def create_structure
|
|
64
104
|
directories.each { |dir| create_dir(dir) }
|
|
65
105
|
files.each { |src, dest| create_file(src, dest) }
|
|
@@ -67,7 +107,7 @@ module Belt
|
|
|
67
107
|
|
|
68
108
|
def directories
|
|
69
109
|
%W[
|
|
70
|
-
#{@app_name}/lambda/controllers
|
|
110
|
+
#{@app_name}/lambda/controllers/api
|
|
71
111
|
#{@app_name}/lambda/models
|
|
72
112
|
#{@app_name}/lambda/models/concerns
|
|
73
113
|
#{@app_name}/lambda/lib/routes
|
|
@@ -81,13 +121,13 @@ module Belt
|
|
|
81
121
|
{
|
|
82
122
|
'Gemfile.erb' => "#{@app_name}/Gemfile",
|
|
83
123
|
'Rakefile.erb' => "#{@app_name}/Rakefile",
|
|
84
|
-
'lambda/api.rb.erb' => "#{@app_name}/lambda
|
|
124
|
+
'lambda/api.rb.erb' => "#{@app_name}/lambda/api.rb",
|
|
85
125
|
'lambda/config/environment.rb.erb' => "#{@app_name}/lambda/config/environment.rb",
|
|
86
126
|
'lambda/models/application_record.rb.erb' => "#{@app_name}/lambda/models/application_record.rb",
|
|
87
127
|
'lambda/models/concerns/timestampable.rb.erb' => "#{@app_name}/lambda/models/concerns/timestampable.rb",
|
|
88
128
|
'lambda/controllers/application_controller.rb.erb' =>
|
|
89
|
-
"#{@app_name}/lambda/controllers
|
|
90
|
-
'lambda/lib/routes/routes.rb.erb' => "#{@app_name}/lambda/lib/routes
|
|
129
|
+
"#{@app_name}/lambda/controllers/api/application_controller.rb",
|
|
130
|
+
'lambda/lib/routes/routes.rb.erb' => "#{@app_name}/lambda/lib/routes/api_routes.rb",
|
|
91
131
|
'infrastructure/routes.tf.rb.erb' => "#{@app_name}/infrastructure/routes.tf.rb",
|
|
92
132
|
'infrastructure/schema.tf.rb.erb' => "#{@app_name}/infrastructure/schema.tf.rb",
|
|
93
133
|
'README.md.erb' => "#{@app_name}/README.md",
|
|
@@ -96,6 +136,84 @@ module Belt
|
|
|
96
136
|
}
|
|
97
137
|
end
|
|
98
138
|
|
|
139
|
+
def generate_environments
|
|
140
|
+
return if @environments.empty?
|
|
141
|
+
|
|
142
|
+
Dir.chdir(@app_name) do
|
|
143
|
+
@environments.each do |env_name|
|
|
144
|
+
Belt::CLI::EnvironmentCommand.new(env_name, quiet: true).generate
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def setup_state
|
|
150
|
+
return if @environments.empty?
|
|
151
|
+
|
|
152
|
+
Dir.chdir(@app_name) do
|
|
153
|
+
# Resolve bucket name — include account ID + region if AWS credentials are available
|
|
154
|
+
if @bucket
|
|
155
|
+
@resolved_bucket = @bucket
|
|
156
|
+
elsif aws_configured?
|
|
157
|
+
region = detect_region
|
|
158
|
+
@resolved_bucket = s3_safe_name("#{@app_name}-terraform-state-#{@aws_account_id}-#{region}")
|
|
159
|
+
else
|
|
160
|
+
@resolved_bucket = s3_safe_name("#{@app_name}-terraform-state")
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Update backend.tf files with the resolved bucket name
|
|
164
|
+
@environments.each do |env_name|
|
|
165
|
+
backend_file = "infrastructure/#{env_name}/backend.tf"
|
|
166
|
+
next unless File.exist?(backend_file)
|
|
167
|
+
|
|
168
|
+
content = File.read(backend_file)
|
|
169
|
+
updated = content.gsub(/bucket\s*=\s*"[^"]+"/, "bucket = \"#{@resolved_bucket}\"")
|
|
170
|
+
File.write(backend_file, updated) if updated != content
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Attempt to actually create the bucket if credentials are available
|
|
174
|
+
if @aws_account_id
|
|
175
|
+
puts "\n Setting up Terraform state bucket..."
|
|
176
|
+
begin
|
|
177
|
+
Belt::CLI::SetupCommand.new(["--bucket", @resolved_bucket]).run_state_setup
|
|
178
|
+
@state_setup_succeeded = true
|
|
179
|
+
rescue SystemExit
|
|
180
|
+
puts ' ⚠ State bucket setup encountered an issue — run `belt setup state` to retry.'
|
|
181
|
+
@state_setup_succeeded = false
|
|
182
|
+
end
|
|
183
|
+
else
|
|
184
|
+
puts "\n State bucket: #{@resolved_bucket}"
|
|
185
|
+
if @aws_error&.include?('ForbiddenException') || @aws_error&.include?('AccessDenied')
|
|
186
|
+
puts " ⚠ AWS credentials found but access denied — check your profile/role configuration."
|
|
187
|
+
puts " #{@aws_error}" if @aws_error
|
|
188
|
+
else
|
|
189
|
+
puts ' ⚠ AWS credentials not detected — skipping state bucket creation.'
|
|
190
|
+
end
|
|
191
|
+
puts ' Run `belt setup state` after configuring credentials (aws sso login / AWS_PROFILE).'
|
|
192
|
+
@state_setup_succeeded = false
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def detect_region
|
|
198
|
+
Dir.glob('infrastructure/*/backend.tf').each do |f|
|
|
199
|
+
match = File.read(f).match(/region\s*=\s*"([^"]+)"/)
|
|
200
|
+
return match[1] if match
|
|
201
|
+
end
|
|
202
|
+
'us-east-1'
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def aws_configured?
|
|
206
|
+
output, status = Open3.capture2e('aws', 'sts', 'get-caller-identity')
|
|
207
|
+
if status.success?
|
|
208
|
+
data = JSON.parse(output) rescue {}
|
|
209
|
+
@aws_account_id = data['Account']
|
|
210
|
+
true
|
|
211
|
+
else
|
|
212
|
+
@aws_error = output.strip
|
|
213
|
+
false
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
99
217
|
def create_dir(dir)
|
|
100
218
|
FileUtils.mkdir_p(dir)
|
|
101
219
|
puts " create #{dir}/"
|
|
@@ -111,17 +229,42 @@ module Belt
|
|
|
111
229
|
def init_git
|
|
112
230
|
Dir.chdir(@app_name) do
|
|
113
231
|
system('git', 'init', '--quiet')
|
|
114
|
-
system('git', 'add', '.')
|
|
115
|
-
system('git', 'commit', '-m', 'Initial commit', '--quiet')
|
|
116
232
|
end
|
|
117
233
|
puts " init #{@app_name}/.git/"
|
|
118
234
|
end
|
|
119
235
|
|
|
236
|
+
def run_bundle_install
|
|
237
|
+
Dir.chdir(@app_name) do
|
|
238
|
+
puts "\n Running bundle install..."
|
|
239
|
+
success = system('bundle', 'install', '--quiet')
|
|
240
|
+
if success
|
|
241
|
+
puts ' ✓ Bundle installed'
|
|
242
|
+
else
|
|
243
|
+
puts ' ⚠ bundle install failed — run it manually after resolving issues.'
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
120
248
|
def generate_frontend
|
|
121
249
|
Dir.chdir(@app_name) do
|
|
122
250
|
Belt::CLI::FrontendCommand.new(@frontend).generate
|
|
123
251
|
end
|
|
124
252
|
end
|
|
253
|
+
|
|
254
|
+
def print_next_steps
|
|
255
|
+
puts "\nNext steps:"
|
|
256
|
+
puts " cd #{@app_name}"
|
|
257
|
+
unless @state_setup_succeeded
|
|
258
|
+
puts ' # Configure AWS credentials (aws sso login / AWS_PROFILE)'
|
|
259
|
+
puts ' belt setup state # Create the S3 state bucket'
|
|
260
|
+
end
|
|
261
|
+
puts ' belt deploy # Deploy to AWS'
|
|
262
|
+
puts ' belt server # Start local frontend server' if @frontend
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def s3_safe_name(name)
|
|
266
|
+
name.to_s.downcase.tr('_', '-')
|
|
267
|
+
end
|
|
125
268
|
end
|
|
126
269
|
end
|
|
127
270
|
end
|