belt 0.1.5 → 0.1.7

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,287 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require_relative 'app_detection'
5
+
6
+ module Belt
7
+ module CLI
8
+ class DestroyCommand
9
+ GENERATORS = %w[scaffold resource model controller environment frontend views].freeze
10
+
11
+ include AppDetection
12
+
13
+ def self.run(args)
14
+ generator = args.shift
15
+
16
+ if generator.nil? || generator =~ /\A-/
17
+ print_help
18
+ exit 0
19
+ end
20
+
21
+ unless GENERATORS.include?(generator)
22
+ puts "Unknown generator: '#{generator}'"
23
+ puts "Available: #{GENERATORS.uniq.join(', ')}"
24
+ puts "\nRun 'belt destroy --help' for usage information."
25
+ exit 1
26
+ end
27
+
28
+ # Normalize: resource is an alias for scaffold
29
+ generator = 'scaffold' if generator == 'resource'
30
+
31
+ case generator
32
+ when 'environment'
33
+ name = args.shift
34
+ if name.nil? || name.empty?
35
+ puts 'Usage: belt destroy environment <name>'
36
+ exit 1
37
+ end
38
+ new(generator, name, []).destroy
39
+ when 'frontend'
40
+ new(generator, nil, []).destroy
41
+ when 'views'
42
+ name = args.shift
43
+ if name.nil? || name.empty?
44
+ puts 'Usage: belt destroy views <name>'
45
+ exit 1
46
+ end
47
+ new(generator, name, args.map { |a| parse_field(a) }).destroy
48
+ else
49
+ name = args.shift
50
+ if name.nil? || name.empty?
51
+ puts "Usage: belt destroy #{generator} <name>"
52
+ exit 1
53
+ end
54
+ new(generator, name, args.map { |a| parse_field(a) }).destroy
55
+ end
56
+ end
57
+
58
+ def self.parse_field(arg)
59
+ name, type = arg.split(':', 2)
60
+ { name: name, type: type || 'string' }
61
+ end
62
+
63
+ def self.print_help
64
+ puts <<~HELP
65
+ Usage: belt destroy <generator> <name> [field:type ...]
66
+ belt d <generator> <name> [field:type ...]
67
+
68
+ Removes files and references created by `belt generate`.
69
+
70
+ Generators:
71
+ scaffold Remove model, controller, routes, schema, and views
72
+ resource Alias for scaffold
73
+ model Remove an ActiveItem model
74
+ controller Remove a controller
75
+ environment Remove a deployment environment directory
76
+ frontend Remove the frontend/ directory
77
+ views Remove React pages for a resource
78
+
79
+ Examples:
80
+ belt d scaffold post title:string body:text status:string
81
+ belt d model user
82
+ belt d controller comments
83
+ belt d environment staging
84
+ belt d frontend
85
+ belt d views post
86
+
87
+ ⚠ This is destructive. Files will be permanently deleted.
88
+ HELP
89
+ end
90
+
91
+ def initialize(generator, name, fields)
92
+ @generator = generator
93
+ @name = name&.downcase&.gsub(/[^a-z0-9_]/, '_')
94
+ @fields = fields
95
+ @app_name = detect_namespace
96
+ @resource_name = if @name
97
+ @name.end_with?('s') ? @name : "#{@name}s"
98
+ end
99
+ @singular_name = if @name
100
+ @name.end_with?('s') ? @name.chomp('s') : @name
101
+ end
102
+ @class_name = @singular_name&.split('_')&.map(&:capitalize)&.join
103
+ @removed = []
104
+ @updated = []
105
+ end
106
+
107
+ def destroy
108
+ case @generator
109
+ when 'scaffold' then destroy_scaffold
110
+ when 'model' then destroy_model
111
+ when 'controller' then destroy_controller
112
+ when 'environment' then destroy_environment
113
+ when 'frontend' then destroy_frontend
114
+ when 'views' then destroy_views
115
+ end
116
+
117
+ print_summary
118
+ end
119
+
120
+ private
121
+
122
+ def destroy_scaffold
123
+ destroy_model
124
+ destroy_controller
125
+ remove_routes
126
+ remove_schema
127
+ destroy_views
128
+ puts "\n✓ Scaffold '#{@singular_name}' destroyed!"
129
+ end
130
+
131
+ def destroy_model
132
+ path = "lambda/models/#{@singular_name}.rb"
133
+ remove_file(path)
134
+ end
135
+
136
+ def destroy_controller
137
+ path = "lambda/controllers/#{@app_name}/#{@resource_name}_controller.rb"
138
+ remove_file(path)
139
+ end
140
+
141
+ def destroy_environment
142
+ dir = "infrastructure/#{@name}"
143
+ if Dir.exist?(dir)
144
+ FileUtils.rm_rf(dir)
145
+ @removed << dir
146
+ puts " remove #{dir}/"
147
+ puts "\n✓ Environment '#{@name}' destroyed!"
148
+ else
149
+ puts "✗ Environment '#{@name}' not found at #{dir}/"
150
+ exit 1
151
+ end
152
+ end
153
+
154
+ def destroy_frontend
155
+ dir = 'frontend'
156
+ if Dir.exist?(dir)
157
+ FileUtils.rm_rf(dir)
158
+ @removed << dir
159
+ puts " remove #{dir}/"
160
+ puts "\n✓ Frontend removed!"
161
+ else
162
+ puts '✗ No frontend/ directory found.'
163
+ exit 1
164
+ end
165
+ end
166
+
167
+ def destroy_views
168
+ return unless @resource_name
169
+
170
+ pages_dir = "frontend/src/pages/#{@resource_name}"
171
+ if Dir.exist?(pages_dir)
172
+ FileUtils.rm_rf(pages_dir)
173
+ @removed << pages_dir
174
+ puts " remove #{pages_dir}/"
175
+ end
176
+
177
+ remove_view_routes
178
+ end
179
+
180
+ def remove_file(path)
181
+ if File.exist?(path)
182
+ File.delete(path)
183
+ @removed << path
184
+ puts " remove #{path}"
185
+
186
+ # Clean up empty parent directories
187
+ dir = File.dirname(path)
188
+ while dir != '.' && Dir.exist?(dir) && Dir.empty?(dir)
189
+ Dir.rmdir(dir)
190
+ puts " remove #{dir}/"
191
+ dir = File.dirname(dir)
192
+ end
193
+ else
194
+ puts " skip #{path} (not found)"
195
+ end
196
+ end
197
+
198
+ def remove_routes
199
+ routes_file = 'infrastructure/routes.tf.rb'
200
+ return unless File.exist?(routes_file)
201
+
202
+ content = File.read(routes_file)
203
+ original = content.dup
204
+
205
+ # Remove "resources :posts" or "resources :posts, tables: [...]" line
206
+ content.gsub!(/^\s*resources :#{@resource_name}.*\n/, '')
207
+
208
+ if content != original
209
+ File.write(routes_file, content)
210
+ @updated << routes_file
211
+ puts " update #{routes_file}"
212
+ end
213
+
214
+ # Update route manifest
215
+ remove_route_manifest_entries
216
+ end
217
+
218
+ def remove_route_manifest_entries
219
+ manifest_file = "lambda/lib/routes/#{@app_name}_routes.rb"
220
+ return unless File.exist?(manifest_file)
221
+
222
+ content = File.read(manifest_file)
223
+ original = content.dup
224
+
225
+ # Remove route entries for this controller
226
+ content.gsub!(/^\s*\{ verb: .+?controller: '#{@resource_name}'.+?\},?\n/, '')
227
+
228
+ # Clean up trailing commas and empty arrays
229
+ content.gsub!(/,(\s*\n\s*\]\.freeze)/, '\1')
230
+
231
+ if content != original
232
+ File.write(manifest_file, content)
233
+ @updated << manifest_file
234
+ puts " update #{manifest_file}"
235
+ end
236
+ end
237
+
238
+ def remove_schema
239
+ schema_file = 'infrastructure/schema.tf.rb'
240
+ return unless File.exist?(schema_file)
241
+
242
+ content = File.read(schema_file)
243
+ original = content.dup
244
+
245
+ # Remove the model block for this resource
246
+ content.gsub!(/\n?\s*model :#{@singular_name} do\n.*?\n\s*end\n?/m, "\n")
247
+
248
+ # Clean up excessive blank lines
249
+ content.gsub!(/\n{3,}/, "\n\n")
250
+
251
+ if content != original
252
+ File.write(schema_file, content)
253
+ @updated << schema_file
254
+ puts " update #{schema_file}"
255
+ end
256
+ end
257
+
258
+ def remove_view_routes
259
+ app_jsx = 'frontend/src/App.jsx'
260
+ return unless File.exist?(app_jsx)
261
+
262
+ content = File.read(app_jsx)
263
+ original = content.dup
264
+
265
+ # Remove import lines for this resource's pages
266
+ content.gsub!(/^import #{@class_name}\w* from '.\/pages\/#{@resource_name}\/.*'\n/, '')
267
+
268
+ # Remove Route elements for this resource
269
+ content.gsub!(/^\s*<Route path="\/#{@resource_name}.*?\/>\n/, '')
270
+
271
+ if content != original
272
+ File.write(app_jsx, content)
273
+ @updated << app_jsx
274
+ puts " update #{app_jsx}"
275
+ end
276
+ end
277
+
278
+ def print_summary
279
+ return if @removed.empty? && @updated.empty?
280
+
281
+ puts "\nSummary:"
282
+ puts " Removed: #{@removed.length} file(s)/dir(s)" unless @removed.empty?
283
+ puts " Updated: #{@updated.length} file(s)" unless @updated.empty?
284
+ end
285
+ end
286
+ end
287
+ 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 ' cd frontend && npm install && npm run dev'
58
- puts ' belt setup frontend <env> # Generate CloudFront + S3 infrastructure'
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,18 @@ 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
+ return if @quiet
43
+
41
44
  puts "\n✓ Frontend infrastructure generated for '#{@env}'!"
42
45
  puts "\nRun `belt apply #{@env}` to create the S3 bucket and CloudFront distribution."
43
46
  puts "Then `belt deploy frontend #{@env}` to build and deploy."
@@ -57,7 +60,7 @@ module Belt
57
60
  template_path = File.join(TEMPLATE_DIR, 'frontend.tf.erb')
58
61
  content = ERB.new(File.read(template_path), trim_mode: '-').result(binding)
59
62
  File.write(dest, content)
60
- puts " create #{dest}"
63
+ puts " create #{dest}" unless @quiet
61
64
  end
62
65
  end
63
66
  end
@@ -11,23 +11,23 @@ module Belt
11
11
  module CLI
12
12
  class GenerateCommand
13
13
  TEMPLATE_DIR = File.expand_path('../../templates/generate', __dir__)
14
- GENERATORS = %w[resource model controller environment frontend views].freeze
14
+ GENERATORS = %w[scaffold resource model controller environment frontend views].freeze
15
15
 
16
16
  include AppDetection
17
17
 
18
18
  FIELD_TYPES = %w[string text integer float boolean date datetime].freeze
19
19
 
20
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]',
21
+ 'scaffold' => {
22
+ description: 'Generate a model, controller, routes, schema, and views for a REST resource.',
23
+ usage: 'belt generate scaffold <name> [field:type ...] [options]',
24
24
  options: [
25
25
  ['--skip-views', 'Skip generating frontend view pages']
26
26
  ],
27
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']
28
+ ['belt g scaffold post title:string body:text', 'Post with title and body fields'],
29
+ ['belt g scaffold comment author:string body:text status:string', 'Scaffold with multiple fields'],
30
+ ['belt g scaffold task --skip-views', 'Scaffold without frontend pages']
31
31
  ],
32
32
  notes: <<~NOTES
33
33
  Creates:
@@ -36,6 +36,9 @@ module Belt
36
36
  infrastructure/routes.tf.rb Route entry added
37
37
  infrastructure/schema.tf.rb DynamoDB table schema added
38
38
  lambda/lib/routes/<app>_routes.rb Route manifest updated
39
+ frontend/src/pages/<names>/ React pages (if frontend exists)
40
+
41
+ Alias: `belt g resource` works the same way.
39
42
  NOTES
40
43
  },
41
44
  'model' => {
@@ -77,12 +80,15 @@ module Belt
77
80
 
78
81
  unless GENERATORS.include?(generator)
79
82
  puts "Unknown generator: '#{generator}'"
80
- puts "Available generators: #{GENERATORS.join(', ')}"
83
+ puts "Available generators: #{(GENERATORS - ['resource']).join(', ')}"
81
84
  puts "\nRun 'belt generate --help' for usage information."
82
85
  exit 1
83
86
  end
84
87
 
85
- # Per-generator help: belt g resource --help
88
+ # Normalize: resource is an alias for scaffold
89
+ generator = 'scaffold' if generator == 'resource'
90
+
91
+ # Per-generator help: belt g scaffold --help
86
92
  if args.include?('--help') || args.include?('-h')
87
93
  print_generator_help(generator)
88
94
  exit 0
@@ -143,18 +149,21 @@ module Belt
143
149
  belt g <generator> <name> [field:type ...] [options]
144
150
 
145
151
  Generators:
146
- resource Generate model, controller, routes, and schema (full REST resource)
152
+ scaffold Generate model, controller, routes, schema, and views (full REST resource)
147
153
  model Generate an ActiveItem model
148
154
  controller Generate a controller
149
155
  environment Create a new deployment environment
150
156
  frontend Scaffold a frontend app (react, vue, svelte)
151
157
  views Generate React pages for a resource
152
158
 
159
+ Aliases:
160
+ resource Same as scaffold
161
+
153
162
  Field Types:
154
163
  #{FIELD_TYPES.join(', ')}
155
164
 
156
165
  Examples:
157
- belt g resource post title:string body:text status:string
166
+ belt g scaffold post title:string body:text status:string
158
167
  belt g model user email:string name:string
159
168
  belt g controller comments
160
169
  belt g environment staging
@@ -210,7 +219,7 @@ module Belt
210
219
  @name = name.downcase.gsub(/[^a-z0-9_]/, '_')
211
220
  @fields = fields
212
221
  @skip_views = skip_views
213
- @app_name = detect_app_name
222
+ @app_name = detect_namespace
214
223
  @module_name = @app_name.split(/[-_]/).map(&:capitalize).join
215
224
  @resource_name = @name.end_with?('s') ? @name : "#{@name}s"
216
225
  @singular_name = @name.end_with?('s') ? @name.chomp('s') : @name
@@ -219,7 +228,7 @@ module Belt
219
228
 
220
229
  def generate
221
230
  case @generator
222
- when 'resource' then generate_resource
231
+ when 'scaffold' then generate_resource
223
232
  when 'model' then generate_model
224
233
  when 'controller' then generate_controller
225
234
  end
@@ -233,7 +242,7 @@ module Belt
233
242
  inject_routes
234
243
  inject_schema
235
244
  generate_views_if_frontend
236
- puts "\n✓ Resource '#{@singular_name}' generated!"
245
+ puts "\n✓ Scaffold '#{@singular_name}' generated!"
237
246
  puts "\nFiles created/updated:"
238
247
  puts " lambda/models/#{@singular_name}.rb"
239
248
  puts " lambda/controllers/#{@app_name}/#{@resource_name}_controller.rb"
@@ -261,14 +270,35 @@ module Belt
261
270
 
262
271
  content = File.read(routes_file)
263
272
  tables_arg = @fields.any? ? ", tables: [:#{@resource_name}]" : ''
273
+ resource_line = "resources :#{@resource_name}#{tables_arg}"
264
274
 
265
- # Insert before the closing `end` of the namespace block
275
+ # Replace the commented placeholder if it exists
266
276
  if content.include?('# resources :posts')
267
- content.sub!('# resources :posts', "resources :#{@resource_name}#{tables_arg}")
268
- elsif content.match?(/namespace :\w+[^\n]*do\n(\s+#[^\n]*\n)*\s+end/)
269
- content.sub!(/^(\s+)(end\s*\z)/m, "\\1 resources :#{@resource_name}#{tables_arg}\n\\1\\2")
277
+ content.sub!('# resources :posts', resource_line)
270
278
  else
271
- content.sub!(/^(\s*end\s*\z)/m, " resources :#{@resource_name}#{tables_arg}\n\\1")
279
+ # Find the target namespace block and insert before its closing `end`
280
+ # Match: `namespace :<name> do` ... `end` (the first `end` at the same indent level)
281
+ namespace_pattern = /^(\s*)namespace :#{Regexp.escape(@app_name)}\b[^\n]*do\s*\n(.*?)^\1end/m
282
+
283
+ if content.match?(namespace_pattern)
284
+ content.sub!(namespace_pattern) do |match|
285
+ indent = $1
286
+ # Insert the resource line before the namespace's closing `end`
287
+ match.sub(/^(#{indent})end\z/m, "#{indent} #{resource_line}\n#{indent}end")
288
+ end
289
+ else
290
+ # Fallback: find any single namespace block and insert into it
291
+ single_ns_pattern = /^(\s*)namespace :\w+\b[^\n]*do\s*\n(.*?)^\1end/m
292
+ if content.match?(single_ns_pattern)
293
+ content.sub!(single_ns_pattern) do |match|
294
+ indent = $1
295
+ match.sub(/^(#{indent})end\z/m, "#{indent} #{resource_line}\n#{indent}end")
296
+ end
297
+ else
298
+ # No namespace block found at all — insert at top level before final end
299
+ content.sub!(/^(\s*end\s*)\z/m, " #{resource_line}\n\\1")
300
+ end
301
+ end
272
302
  end
273
303
 
274
304
  File.write(routes_file, content)
@@ -82,8 +82,8 @@ module Belt
82
82
 
83
83
  puts "Creating new Belt application: #{@app_name}"
84
84
  create_structure
85
- generate_frontend if @frontend
86
85
  generate_environments
86
+ generate_frontend if @frontend
87
87
  init_git
88
88
  run_bundle_install
89
89
  setup_state
@@ -107,7 +107,7 @@ module Belt
107
107
 
108
108
  def directories
109
109
  %W[
110
- #{@app_name}/lambda/controllers/#{@app_name}
110
+ #{@app_name}/lambda/controllers/api
111
111
  #{@app_name}/lambda/models
112
112
  #{@app_name}/lambda/models/concerns
113
113
  #{@app_name}/lambda/lib/routes
@@ -121,13 +121,13 @@ module Belt
121
121
  {
122
122
  'Gemfile.erb' => "#{@app_name}/Gemfile",
123
123
  'Rakefile.erb' => "#{@app_name}/Rakefile",
124
- 'lambda/api.rb.erb' => "#{@app_name}/lambda/#{@app_name}.rb",
124
+ 'lambda/api.rb.erb' => "#{@app_name}/lambda/api.rb",
125
125
  'lambda/config/environment.rb.erb' => "#{@app_name}/lambda/config/environment.rb",
126
126
  'lambda/models/application_record.rb.erb' => "#{@app_name}/lambda/models/application_record.rb",
127
127
  'lambda/models/concerns/timestampable.rb.erb' => "#{@app_name}/lambda/models/concerns/timestampable.rb",
128
128
  'lambda/controllers/application_controller.rb.erb' =>
129
- "#{@app_name}/lambda/controllers/#{@app_name}/application_controller.rb",
130
- 'lambda/lib/routes/routes.rb.erb' => "#{@app_name}/lambda/lib/routes/#{@app_name}_routes.rb",
129
+ "#{@app_name}/lambda/controllers/api/application_controller.rb",
130
+ 'lambda/lib/routes/routes.rb.erb' => "#{@app_name}/lambda/lib/routes/api_routes.rb",
131
131
  'infrastructure/routes.tf.rb.erb' => "#{@app_name}/infrastructure/routes.tf.rb",
132
132
  'infrastructure/schema.tf.rb.erb' => "#{@app_name}/infrastructure/schema.tf.rb",
133
133
  'README.md.erb' => "#{@app_name}/README.md",
@@ -16,6 +16,10 @@ module Belt
16
16
 
17
17
  return non_param.map { |s| s.gsub('-', '_') }.join('/') if route.resource? && nested_resource?(segments)
18
18
 
19
+ # For non-resource routes with a single segment (e.g., post '/signup' in :onboarding),
20
+ # the segment is the action name, not the controller. Use the gateway name as controller.
21
+ return gateway.name if !route.resource? && non_param.length == 1
22
+
19
23
  non_param.first.gsub('-', '_')
20
24
  end
21
25
 
@@ -144,7 +144,7 @@ module Belt
144
144
  subtitle = ENV.fetch('WELCOME_SUBTITLE', 'Your app is ready. Deploy to see it live on AWS.')
145
145
 
146
146
  css = load_asset('welcome.css') || ''
147
- image_b64 = load_image_base64('belt-default.png') || ''
147
+ image_b64 = load_image_base64('belt-default.jpg') || ''
148
148
 
149
149
  if @api_url
150
150
  deployed_html(css: css, image_b64: image_b64)
@@ -175,7 +175,7 @@ module Belt
175
175
  </head>
176
176
  <body>
177
177
  <div class="hero">
178
- <img src="data:image/png;base64,#{image_b64}" alt="Belt" class="hero-bg" />
178
+ <img src="data:image/jpeg;base64,#{image_b64}" alt="Belt" class="hero-bg" />
179
179
  <div class="overlay">
180
180
  <h1>#{@app_name}</h1>
181
181
  <p class="subtitle">Deployed and running on AWS</p>
@@ -208,7 +208,7 @@ module Belt
208
208
  </head>
209
209
  <body>
210
210
  <div class="hero">
211
- <img src="data:image/png;base64,#{image_b64}" alt="Belt" class="hero-bg" />
211
+ <img src="data:image/jpeg;base64,#{image_b64}" alt="Belt" class="hero-bg" />
212
212
  <div class="overlay">
213
213
  <h1>#{title}</h1>
214
214
  <p class="subtitle">#{subtitle}</p>
data/lib/belt/cli.rb CHANGED
@@ -5,6 +5,7 @@ require_relative 'root'
5
5
  require_relative 'cli/env_resolver'
6
6
  require_relative 'cli/new_command'
7
7
  require_relative 'cli/generate_command'
8
+ require_relative 'cli/destroy_command'
8
9
  require_relative 'cli/frontend_command'
9
10
  require_relative 'cli/frontend_setup_command'
10
11
  require_relative 'cli/frontend_deploy_command'
@@ -22,6 +23,7 @@ module Belt
22
23
  COMMANDS_DEFINITION = {
23
24
  'new' => Belt::CLI::NewCommand,
24
25
  %w[generate g] => Belt::CLI::GenerateCommand,
26
+ %w[destroy d] => Belt::CLI::DestroyCommand,
25
27
  'routes' => Belt::CLI::RoutesCommand,
26
28
  %w[console c] => Belt::CLI::ConsoleCommand,
27
29
  %w[tasks --tasks -T] => Belt::CLI::TasksCommand,
@@ -45,7 +47,21 @@ module Belt
45
47
  exit 1
46
48
  end
47
49
 
48
- # Terraform shorthand: belt init wups, belt plan wups, belt apply wups
50
+ # `belt destroy` is ambiguous: could be terraform destroy or belt destroy <generator>.
51
+ # Route to DestroyCommand if the first arg is a known generator or help flag.
52
+ if command == 'destroy'
53
+ if args.empty? || args.first =~ /\A-/
54
+ # belt destroy --help → DestroyCommand help
55
+ # belt destroy (no args) → terraform destroy (needs env)
56
+ if args.include?('--help') || args.include?('-h')
57
+ return DestroyCommand.run(args)
58
+ end
59
+ elsif DestroyCommand::GENERATORS.include?(args.first)
60
+ return DestroyCommand.run(args)
61
+ end
62
+ end
63
+
64
+ # Terraform shorthand: belt init wups, belt plan wups, belt apply wups, belt destroy wups
49
65
  return Belt::CLI::TerraformCommand.run(command, args) if TERRAFORM_ACTIONS.include?(command)
50
66
 
51
67
  handler = COMMANDS[command]
@@ -71,10 +87,14 @@ module Belt
71
87
 
72
88
  Commands:
73
89
  new <app_name> [--frontend react] Create a new Belt application
74
- generate <resource|model|controller> <name> Generate components
90
+ generate <scaffold|model|controller> <name> Generate components
75
91
  generate frontend <react|vue|svelte> Scaffold a frontend app
76
92
  generate views <resource> [fields...] Generate React pages for REST actions
77
93
  generate environment <name> Create a new environment
94
+ destroy <scaffold|model|controller> <name> Remove generated components
95
+ destroy frontend Remove the frontend/ directory
96
+ destroy views <resource> Remove React pages for a resource
97
+ destroy environment <name> Remove an environment directory
78
98
  server Start local dev server (frontend)
79
99
  s Alias for server
80
100
  deploy [environment] Deploy to AWS (init → plan → apply)
@@ -106,7 +126,8 @@ module Belt
106
126
 
107
127
  Examples:
108
128
  belt new blog --frontend react
109
- belt generate resource post title:string content:text status:string
129
+ belt generate scaffold post title:string content:text status:string
130
+ belt destroy scaffold post
110
131
  belt generate frontend react
111
132
  belt server # Start local frontend server
112
133
  belt deploy # Deploy dev to AWS