belt 0.1.6 → 0.1.8
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/cli/app_detection.rb +6 -0
- data/lib/belt/cli/deploy_command.rb +15 -0
- data/lib/belt/cli/destroy_command.rb +320 -0
- data/lib/belt/cli/environment_command.rb +11 -0
- data/lib/belt/cli/frontend_command.rb +35 -3
- data/lib/belt/cli/frontend_setup_command.rb +35 -2
- data/lib/belt/cli/generate_command.rb +79 -38
- data/lib/belt/cli/new_command.rb +1 -3
- data/lib/belt/cli/routes_command/route_inference.rb +4 -0
- data/lib/belt/cli/routes_command.rb +1 -9
- data/lib/belt/cli/tables_command.rb +53 -15
- data/lib/belt/cli/views_command.rb +11 -8
- data/lib/belt/cli.rb +24 -3
- data/lib/belt/inflector.rb +58 -0
- data/lib/belt/lambda_handler.rb +11 -0
- data/lib/belt/route_dsl.rb +3 -11
- data/lib/belt/table_inference.rb +4 -18
- data/lib/belt/version.rb +1 -1
- data/lib/templates/environment/main.tf.erb +1 -1
- data/lib/templates/environment/outputs.tf.erb +5 -0
- data/lib/templates/frontend_infra/frontend.tf.erb +0 -4
- data/lib/templates/generate/controller.rb.erb +0 -1
- data/lib/templates/generate/model.rb.erb +0 -2
- data/lib/templates/new_app/lambda/config/environment.rb.erb +7 -1
- metadata +17 -2
- data/lib/templates/new_app/lambda/models/concerns/timestampable.rb.erb +0 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebbefe3f44abda51ac10b219cda18678ca834d06d8eab39e31beb3e48e99b25a
|
|
4
|
+
data.tar.gz: 3233b91c7297982ef7f5037331ef003c3637ebc6dccf4321b4a5f758ed57f1d9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: af38bf5330f69577f37ab34a0bd7510b5d84aa14969efd8975ac6875ea85450aaa741beeb71bf3f51f627af11c0c5445806081156706cdfd4901fba4301e33ed
|
|
7
|
+
data.tar.gz: 2eade1680ebfe76e78ab38e38d273565cf34eb1f1789427d358706ac2c4c128a5f86448a5eb2a146161be8c912bf53f9229fceb0904719c9df5ef3444dc3481e
|
|
@@ -28,6 +28,12 @@ module Belt
|
|
|
28
28
|
File.basename(Dir.pwd)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
# Detects existing environments by scanning infrastructure/ directories.
|
|
32
|
+
# Each subdirectory with a main.tf is considered an environment.
|
|
33
|
+
def detect_environments
|
|
34
|
+
Dir.glob('infrastructure/*/main.tf').map { |f| File.basename(File.dirname(f)) }.sort
|
|
35
|
+
end
|
|
36
|
+
|
|
31
37
|
# S3 bucket names follow DNS rules — underscores are not allowed.
|
|
32
38
|
# App names often use underscores (Ruby namespaces); convert for S3.
|
|
33
39
|
def s3_safe_name(name)
|
|
@@ -127,6 +127,9 @@ module Belt
|
|
|
127
127
|
|
|
128
128
|
puts "\n✅ Deployed #{@env} successfully!"
|
|
129
129
|
print_outputs(env_dir)
|
|
130
|
+
|
|
131
|
+
deploy_frontend_if_exists
|
|
132
|
+
|
|
130
133
|
puts "\n Run `belt server` to view your app locally (auto-connects to the deployed API)."
|
|
131
134
|
end
|
|
132
135
|
|
|
@@ -479,6 +482,18 @@ module Belt
|
|
|
479
482
|
File.delete('tfplan') if File.exist?('tfplan')
|
|
480
483
|
end
|
|
481
484
|
|
|
485
|
+
def deploy_frontend_if_exists
|
|
486
|
+
frontend_dir = File.join(@project_root, 'frontend')
|
|
487
|
+
return unless Dir.exist?(frontend_dir) && File.exist?(File.join(frontend_dir, 'package.json'))
|
|
488
|
+
|
|
489
|
+
puts "\n━━━ frontend deploy ━━━"
|
|
490
|
+
require_relative 'frontend_deploy_command'
|
|
491
|
+
Belt::CLI::FrontendDeployCommand.new(@env).run
|
|
492
|
+
rescue StandardError => e
|
|
493
|
+
puts "\n ⚠ Frontend deploy failed: #{e.message}"
|
|
494
|
+
puts " Run `belt deploy frontend #{@env}` manually to retry."
|
|
495
|
+
end
|
|
496
|
+
|
|
482
497
|
def print_outputs(env_dir)
|
|
483
498
|
Dir.chdir(env_dir) do
|
|
484
499
|
output = `terraform output 2>/dev/null`.strip
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require_relative 'app_detection'
|
|
5
|
+
require_relative 'tables_command'
|
|
6
|
+
require_relative '../inflector'
|
|
7
|
+
|
|
8
|
+
module Belt
|
|
9
|
+
module CLI
|
|
10
|
+
class DestroyCommand
|
|
11
|
+
GENERATORS = %w[scaffold resource model controller environment frontend views].freeze
|
|
12
|
+
|
|
13
|
+
include AppDetection
|
|
14
|
+
|
|
15
|
+
def self.run(args)
|
|
16
|
+
generator = args.shift
|
|
17
|
+
|
|
18
|
+
if generator.nil? || generator =~ /\A-/
|
|
19
|
+
print_help
|
|
20
|
+
exit 0
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
unless GENERATORS.include?(generator)
|
|
24
|
+
puts "Unknown generator: '#{generator}'"
|
|
25
|
+
puts "Available: #{GENERATORS.uniq.join(', ')}"
|
|
26
|
+
puts "\nRun 'belt destroy --help' for usage information."
|
|
27
|
+
exit 1
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Normalize: resource is an alias for scaffold
|
|
31
|
+
generator = 'scaffold' if generator == 'resource'
|
|
32
|
+
|
|
33
|
+
case generator
|
|
34
|
+
when 'environment'
|
|
35
|
+
name = args.shift
|
|
36
|
+
if name.nil? || name.empty?
|
|
37
|
+
puts 'Usage: belt destroy environment <name>'
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
new(generator, name, []).destroy
|
|
41
|
+
when 'frontend'
|
|
42
|
+
new(generator, nil, []).destroy
|
|
43
|
+
when 'views'
|
|
44
|
+
name = args.shift
|
|
45
|
+
if name.nil? || name.empty?
|
|
46
|
+
puts 'Usage: belt destroy views <name>'
|
|
47
|
+
exit 1
|
|
48
|
+
end
|
|
49
|
+
new(generator, name, args.map { |a| parse_field(a) }).destroy
|
|
50
|
+
else
|
|
51
|
+
name = args.shift
|
|
52
|
+
if name.nil? || name.empty?
|
|
53
|
+
puts "Usage: belt destroy #{generator} <name>"
|
|
54
|
+
exit 1
|
|
55
|
+
end
|
|
56
|
+
new(generator, name, args.map { |a| parse_field(a) }).destroy
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.parse_field(arg)
|
|
61
|
+
name, type = arg.split(':', 2)
|
|
62
|
+
{ name: name, type: type || 'string' }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.print_help
|
|
66
|
+
puts <<~HELP
|
|
67
|
+
Usage: belt destroy <generator> <name> [field:type ...]
|
|
68
|
+
belt d <generator> <name> [field:type ...]
|
|
69
|
+
|
|
70
|
+
Removes files and references created by `belt generate`.
|
|
71
|
+
|
|
72
|
+
Generators:
|
|
73
|
+
scaffold Remove model, controller, routes, schema, and views
|
|
74
|
+
resource Alias for scaffold
|
|
75
|
+
model Remove an ActiveItem model
|
|
76
|
+
controller Remove a controller
|
|
77
|
+
environment Remove a deployment environment directory
|
|
78
|
+
frontend Remove the frontend/ directory
|
|
79
|
+
views Remove React pages for a resource
|
|
80
|
+
|
|
81
|
+
Examples:
|
|
82
|
+
belt d scaffold post title:string body:text status:string
|
|
83
|
+
belt d model user
|
|
84
|
+
belt d controller comments
|
|
85
|
+
belt d environment staging
|
|
86
|
+
belt d frontend
|
|
87
|
+
belt d views post
|
|
88
|
+
|
|
89
|
+
⚠ This is destructive. Files will be permanently deleted.
|
|
90
|
+
HELP
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def initialize(generator, name, fields)
|
|
94
|
+
@generator = generator
|
|
95
|
+
@name = name&.downcase&.gsub(/[^a-z0-9_]/, '_')
|
|
96
|
+
@fields = fields
|
|
97
|
+
@app_name = detect_namespace
|
|
98
|
+
@singular_name = @name ? Belt::Inflector.singularize(@name) : nil
|
|
99
|
+
@resource_name = @singular_name ? Belt::Inflector.pluralize(@singular_name) : nil
|
|
100
|
+
@class_name = @singular_name ? Belt::Inflector.classify(@singular_name) : nil
|
|
101
|
+
@removed = []
|
|
102
|
+
@updated = []
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def destroy
|
|
106
|
+
case @generator
|
|
107
|
+
when 'scaffold' then destroy_scaffold
|
|
108
|
+
when 'model' then destroy_model
|
|
109
|
+
when 'controller' then destroy_controller
|
|
110
|
+
when 'environment' then destroy_environment
|
|
111
|
+
when 'frontend' then destroy_frontend
|
|
112
|
+
when 'views' then destroy_views
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
print_summary
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def destroy_scaffold
|
|
121
|
+
destroy_model
|
|
122
|
+
destroy_controller
|
|
123
|
+
remove_routes
|
|
124
|
+
remove_schema
|
|
125
|
+
sync_tables
|
|
126
|
+
destroy_views
|
|
127
|
+
puts "\n✓ Scaffold '#{@singular_name}' destroyed!"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def destroy_model
|
|
131
|
+
path = "lambda/models/#{@singular_name}.rb"
|
|
132
|
+
remove_file(path)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def destroy_controller
|
|
136
|
+
path = "lambda/controllers/#{@app_name}/#{@resource_name}_controller.rb"
|
|
137
|
+
remove_file(path)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def destroy_environment
|
|
141
|
+
dir = "infrastructure/#{@name}"
|
|
142
|
+
if Dir.exist?(dir)
|
|
143
|
+
FileUtils.rm_rf(dir)
|
|
144
|
+
@removed << dir
|
|
145
|
+
puts " remove #{dir}/"
|
|
146
|
+
puts "\n✓ Environment '#{@name}' destroyed!"
|
|
147
|
+
else
|
|
148
|
+
puts "✗ Environment '#{@name}' not found at #{dir}/"
|
|
149
|
+
exit 1
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def destroy_frontend
|
|
154
|
+
dir = 'frontend'
|
|
155
|
+
if Dir.exist?(dir)
|
|
156
|
+
FileUtils.rm_rf(dir)
|
|
157
|
+
@removed << dir
|
|
158
|
+
puts " remove #{dir}/"
|
|
159
|
+
|
|
160
|
+
# Remove frontend.tf and revert frontend_urls in main.tf for each environment
|
|
161
|
+
Dir.glob('infrastructure/*/frontend.tf').each do |frontend_tf|
|
|
162
|
+
FileUtils.rm_f(frontend_tf)
|
|
163
|
+
puts " remove #{frontend_tf}"
|
|
164
|
+
|
|
165
|
+
env_dir = File.dirname(frontend_tf)
|
|
166
|
+
main_tf = File.join(env_dir, 'main.tf')
|
|
167
|
+
revert_main_tf_frontend_urls(main_tf)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
puts "\n✓ Frontend removed!"
|
|
171
|
+
else
|
|
172
|
+
puts '✗ No frontend/ directory found.'
|
|
173
|
+
exit 1
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def revert_main_tf_frontend_urls(main_tf)
|
|
178
|
+
return unless File.exist?(main_tf)
|
|
179
|
+
|
|
180
|
+
content = File.read(main_tf)
|
|
181
|
+
return unless content.include?('aws_cloudfront_distribution.frontend.domain_name')
|
|
182
|
+
|
|
183
|
+
# Replace the multi-line concat block back to a simple conditional
|
|
184
|
+
replaced = content.sub(
|
|
185
|
+
/^\s*frontend_urls\s*=\s*concat\(\n(?:.*\n)*?\s*\)\s*\n/,
|
|
186
|
+
" frontend_urls = var.environment == \"prod\" ? [] : [\"http://localhost:3000\"]\n"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if replaced != content
|
|
190
|
+
File.write(main_tf, replaced)
|
|
191
|
+
puts " update #{main_tf} (reverted frontend_urls)"
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def destroy_views
|
|
196
|
+
return unless @resource_name
|
|
197
|
+
|
|
198
|
+
pages_dir = "frontend/src/pages/#{@resource_name}"
|
|
199
|
+
|
|
200
|
+
if Dir.exist?(pages_dir)
|
|
201
|
+
FileUtils.rm_rf(pages_dir)
|
|
202
|
+
@removed << pages_dir
|
|
203
|
+
puts " remove #{pages_dir}/"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
remove_view_routes
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def remove_file(path)
|
|
210
|
+
if File.exist?(path)
|
|
211
|
+
File.delete(path)
|
|
212
|
+
@removed << path
|
|
213
|
+
puts " remove #{path}"
|
|
214
|
+
|
|
215
|
+
# Clean up empty parent directories
|
|
216
|
+
dir = File.dirname(path)
|
|
217
|
+
while dir != '.' && Dir.exist?(dir) && Dir.empty?(dir)
|
|
218
|
+
Dir.rmdir(dir)
|
|
219
|
+
puts " remove #{dir}/"
|
|
220
|
+
dir = File.dirname(dir)
|
|
221
|
+
end
|
|
222
|
+
else
|
|
223
|
+
puts " skip #{path} (not found)"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def remove_routes
|
|
228
|
+
routes_file = 'infrastructure/routes.tf.rb'
|
|
229
|
+
return unless File.exist?(routes_file)
|
|
230
|
+
|
|
231
|
+
content = File.read(routes_file)
|
|
232
|
+
original = content.dup
|
|
233
|
+
|
|
234
|
+
# Remove "resources :posts" or "resources :posts, tables: [...]" line
|
|
235
|
+
content.gsub!(/^\s*resources :#{@resource_name}.*\n/, '')
|
|
236
|
+
|
|
237
|
+
if content != original
|
|
238
|
+
File.write(routes_file, content)
|
|
239
|
+
@updated << routes_file
|
|
240
|
+
puts " update #{routes_file}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Update route manifest
|
|
244
|
+
remove_route_manifest_entries
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def remove_route_manifest_entries
|
|
248
|
+
manifest_file = "lambda/lib/routes/#{@app_name}_routes.rb"
|
|
249
|
+
return unless File.exist?(manifest_file)
|
|
250
|
+
|
|
251
|
+
content = File.read(manifest_file)
|
|
252
|
+
original = content.dup
|
|
253
|
+
|
|
254
|
+
# Remove route entries for this controller
|
|
255
|
+
content.gsub!(/^\s*\{ verb: .+?controller: '#{@resource_name}'.+?\},?\n/, '')
|
|
256
|
+
|
|
257
|
+
# Clean up trailing commas and empty arrays
|
|
258
|
+
content.gsub!(/,(\s*\n\s*\]\.freeze)/, '\1')
|
|
259
|
+
|
|
260
|
+
if content != original
|
|
261
|
+
File.write(manifest_file, content)
|
|
262
|
+
@updated << manifest_file
|
|
263
|
+
puts " update #{manifest_file}"
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def remove_schema
|
|
268
|
+
schema_file = 'infrastructure/schema.tf.rb'
|
|
269
|
+
return unless File.exist?(schema_file)
|
|
270
|
+
|
|
271
|
+
content = File.read(schema_file)
|
|
272
|
+
original = content.dup
|
|
273
|
+
|
|
274
|
+
# Remove the model block for this resource
|
|
275
|
+
content.gsub!(/\n?\s*model :#{@singular_name} do\n.*?\n\s*end\n?/m, "\n")
|
|
276
|
+
|
|
277
|
+
# Clean up excessive blank lines
|
|
278
|
+
content.gsub!(/\n{3,}/, "\n\n")
|
|
279
|
+
|
|
280
|
+
if content != original
|
|
281
|
+
File.write(schema_file, content)
|
|
282
|
+
@updated << schema_file
|
|
283
|
+
puts " update #{schema_file}"
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def sync_tables
|
|
288
|
+
Belt::CLI::TablesCommand.sync_all_environments
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def remove_view_routes
|
|
292
|
+
app_jsx = 'frontend/src/App.jsx'
|
|
293
|
+
return unless File.exist?(app_jsx)
|
|
294
|
+
|
|
295
|
+
content = File.read(app_jsx)
|
|
296
|
+
original = content.dup
|
|
297
|
+
|
|
298
|
+
# Remove import lines for this resource's pages
|
|
299
|
+
content.gsub!(/^import #{@class_name}\w* from '.\/pages\/#{@resource_name}\/.*'\n/, '')
|
|
300
|
+
|
|
301
|
+
# Remove Route elements for this resource
|
|
302
|
+
content.gsub!(/^\s*<Route path="\/#{@resource_name}.*?\/>\n/, '')
|
|
303
|
+
|
|
304
|
+
if content != original
|
|
305
|
+
File.write(app_jsx, content)
|
|
306
|
+
@updated << app_jsx
|
|
307
|
+
puts " update #{app_jsx}"
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def print_summary
|
|
312
|
+
return if @removed.empty? && @updated.empty?
|
|
313
|
+
|
|
314
|
+
puts "\nSummary:"
|
|
315
|
+
puts " Removed: #{@removed.length} file(s)/dir(s)" unless @removed.empty?
|
|
316
|
+
puts " Updated: #{@updated.length} file(s)" unless @updated.empty?
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
@@ -51,6 +51,8 @@ module Belt
|
|
|
51
51
|
|
|
52
52
|
puts "\n✓ Environment '#{@env_name}' created!"
|
|
53
53
|
|
|
54
|
+
setup_frontend_if_exists
|
|
55
|
+
|
|
54
56
|
unless @quiet
|
|
55
57
|
puts "\nNext steps:"
|
|
56
58
|
puts " cd #{dest_dir}"
|
|
@@ -62,6 +64,15 @@ module Belt
|
|
|
62
64
|
|
|
63
65
|
private
|
|
64
66
|
|
|
67
|
+
def setup_frontend_if_exists
|
|
68
|
+
return unless Dir.exist?('frontend') && !Dir.empty?('frontend')
|
|
69
|
+
|
|
70
|
+
puts "\n Frontend detected — generating frontend infrastructure..."
|
|
71
|
+
require_relative 'frontend_setup_command'
|
|
72
|
+
FrontendSetupCommand.new(@env_name, quiet: true).run
|
|
73
|
+
puts " create infrastructure/#{@env_name}/frontend.tf"
|
|
74
|
+
end
|
|
75
|
+
|
|
65
76
|
def templates
|
|
66
77
|
{
|
|
67
78
|
'main.tf.erb' => 'main.tf',
|
|
@@ -53,14 +53,46 @@ module Belt
|
|
|
53
53
|
copy_template(framework_dir, dest_dir)
|
|
54
54
|
|
|
55
55
|
puts "\n✓ Frontend (#{@framework}) created in frontend/"
|
|
56
|
+
|
|
57
|
+
install_dependencies(dest_dir)
|
|
58
|
+
setup_frontend_infra_for_existing_environments
|
|
59
|
+
|
|
56
60
|
puts "\nNext steps:"
|
|
57
|
-
puts '
|
|
58
|
-
puts ' belt
|
|
59
|
-
puts ' belt deploy frontend <env> # Build and deploy to AWS'
|
|
61
|
+
puts ' belt server # Start local dev server'
|
|
62
|
+
puts ' belt deploy # Deploy everything to AWS'
|
|
60
63
|
end
|
|
61
64
|
|
|
62
65
|
private
|
|
63
66
|
|
|
67
|
+
def install_dependencies(dest_dir)
|
|
68
|
+
puts "\n Installing npm dependencies..."
|
|
69
|
+
success = system('npm', 'install', '--prefix', dest_dir, '--loglevel', 'error')
|
|
70
|
+
if success
|
|
71
|
+
puts " ✓ Dependencies installed"
|
|
72
|
+
else
|
|
73
|
+
puts " ⚠ npm install failed — run `cd #{dest_dir} && npm install` manually"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def setup_frontend_infra_for_existing_environments
|
|
78
|
+
environments = detect_environments
|
|
79
|
+
return if environments.empty?
|
|
80
|
+
|
|
81
|
+
puts "\n Setting up frontend infrastructure for existing environments..."
|
|
82
|
+
require_relative 'frontend_setup_command'
|
|
83
|
+
|
|
84
|
+
environments.each do |env_name|
|
|
85
|
+
frontend_tf = "infrastructure/#{env_name}/frontend.tf"
|
|
86
|
+
if File.exist?(frontend_tf)
|
|
87
|
+
puts " skip #{frontend_tf} (already exists)"
|
|
88
|
+
next
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
FrontendSetupCommand.new(env_name, quiet: true).run
|
|
92
|
+
puts " create #{frontend_tf}"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
64
96
|
def copy_template(src_dir, dest_dir)
|
|
65
97
|
Dir.glob("#{src_dir}/**/*", File::FNM_DOTMATCH).each do |src|
|
|
66
98
|
next if File.directory?(src)
|
|
@@ -29,15 +29,19 @@ module Belt
|
|
|
29
29
|
new(env).run
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
def initialize(env)
|
|
32
|
+
def initialize(env, quiet: false)
|
|
33
33
|
@env = env
|
|
34
34
|
@app_name = detect_app_name
|
|
35
35
|
@env_dir = "infrastructure/#{@env}"
|
|
36
|
+
@quiet = quiet
|
|
36
37
|
end
|
|
37
38
|
|
|
38
39
|
def run
|
|
39
40
|
validate!
|
|
40
41
|
generate_frontend_tf
|
|
42
|
+
update_main_tf_frontend_urls
|
|
43
|
+
return if @quiet
|
|
44
|
+
|
|
41
45
|
puts "\n✓ Frontend infrastructure generated for '#{@env}'!"
|
|
42
46
|
puts "\nRun `belt apply #{@env}` to create the S3 bucket and CloudFront distribution."
|
|
43
47
|
puts "Then `belt deploy frontend #{@env}` to build and deploy."
|
|
@@ -57,7 +61,36 @@ module Belt
|
|
|
57
61
|
template_path = File.join(TEMPLATE_DIR, 'frontend.tf.erb')
|
|
58
62
|
content = ERB.new(File.read(template_path), trim_mode: '-').result(binding)
|
|
59
63
|
File.write(dest, content)
|
|
60
|
-
puts " create #{dest}"
|
|
64
|
+
puts " create #{dest}" unless @quiet
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def update_main_tf_frontend_urls
|
|
68
|
+
main_tf = File.join(@env_dir, 'main.tf')
|
|
69
|
+
return unless File.exist?(main_tf)
|
|
70
|
+
|
|
71
|
+
content = File.read(main_tf)
|
|
72
|
+
cloudfront_url = '"https://${aws_cloudfront_distribution.frontend.domain_name}"'
|
|
73
|
+
|
|
74
|
+
# Already patched — skip
|
|
75
|
+
return if content.include?('aws_cloudfront_distribution.frontend.domain_name')
|
|
76
|
+
|
|
77
|
+
# Match any frontend_urls line (hardcoded list or conditional expression)
|
|
78
|
+
new_frontend_urls = <<~HCL.chomp
|
|
79
|
+
frontend_urls = concat(
|
|
80
|
+
[#{cloudfront_url}],
|
|
81
|
+
var.environment == "prod" ? [] : ["http://localhost:3000"]
|
|
82
|
+
)
|
|
83
|
+
HCL
|
|
84
|
+
|
|
85
|
+
replaced = content.sub(
|
|
86
|
+
/^(\s*)frontend_urls\s*=\s*.+$/,
|
|
87
|
+
new_frontend_urls
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if replaced != content
|
|
91
|
+
File.write(main_tf, replaced)
|
|
92
|
+
puts " update #{main_tf} (added CloudFront URL to frontend_urls)" unless @quiet
|
|
93
|
+
end
|
|
61
94
|
end
|
|
62
95
|
end
|
|
63
96
|
end
|