belt 0.1.4 → 0.1.5

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.
@@ -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
- args.each do |arg|
16
- if arg.start_with?('--frontend')
17
- frontend = if arg.include?('=')
18
- arg.split('=', 2).last
19
- else
20
- args[args.index(arg) + 1]
21
- end
22
- elsif !arg.start_with?('-')
23
- app_name ||= arg
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> [--frontend react|vue|svelte]'
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
- puts "\nNext steps:"
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) }
@@ -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
@@ -0,0 +1,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'json'
5
+ require_relative 'app_detection'
6
+ require_relative 'terraform_command'
7
+
8
+ module Belt
9
+ module CLI
10
+ class ServerCommand
11
+ include AppDetection
12
+
13
+ DEFAULT_PORT = 3000
14
+
15
+ def self.run(args)
16
+ port = DEFAULT_PORT
17
+ open_browser = false
18
+
19
+ i = 0
20
+ while i < args.length
21
+ case args[i]
22
+ when '-p', '--port'
23
+ i += 1
24
+ port = args[i].to_i
25
+ when /^--port=/
26
+ port = args[i].split('=', 2).last.to_i
27
+ when '-o', '--open'
28
+ open_browser = true
29
+ when '-h', '--help'
30
+ puts help_text
31
+ exit 0
32
+ end
33
+ i += 1
34
+ end
35
+
36
+ new(port: port, open_browser: open_browser).run
37
+ end
38
+
39
+ def self.help_text
40
+ <<~HELP
41
+ Start a local development server for the frontend.
42
+
43
+ Usage: belt server [options]
44
+ belt s [options]
45
+
46
+ Options:
47
+ -p, --port PORT Port to serve on (default: #{DEFAULT_PORT})
48
+ -o, --open Open browser after starting
49
+ -h, --help Show this help
50
+
51
+ Behavior:
52
+ • If frontend/ exists → runs the frontend dev server (npm run dev)
53
+ Automatically sets VITE_API_URL from terraform outputs if deployed.
54
+ • If no frontend → serves the welcome page via a local HTTP server
55
+ After deploy, shows live API URL and deployment status.
56
+
57
+ Note: The backend is serverless (AWS Lambda). Use `belt deploy` to deploy
58
+ your backend to AWS. Local frontend development automatically points to
59
+ your deployed API via VITE_API_URL.
60
+
61
+ Examples:
62
+ belt server # Start on port #{DEFAULT_PORT}
63
+ belt s -p 4000 # Start on port 4000
64
+ belt s --open # Start and open browser
65
+ HELP
66
+ end
67
+
68
+ def initialize(port:, open_browser: false)
69
+ @port = port
70
+ @open_browser = open_browser
71
+ @app_name = detect_app_name
72
+ @api_url = detect_api_url
73
+ end
74
+
75
+ def run
76
+ if Dir.exist?('frontend') && File.exist?('frontend/package.json')
77
+ run_frontend_dev_server
78
+ else
79
+ run_welcome_server
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def run_frontend_dev_server
86
+ puts "🚀 Starting frontend dev server on port #{@port}..."
87
+ if @api_url
88
+ puts " Backend API: #{@api_url}"
89
+ else
90
+ puts ' Backend is serverless — deploy with `belt deploy` to set up AWS resources.'
91
+ end
92
+ puts ''
93
+
94
+ open_browser_later if @open_browser
95
+
96
+ env = { 'PORT' => @port.to_s }
97
+ env['VITE_API_URL'] = @api_url if @api_url
98
+
99
+ # Prefer the dev script with the port flag for Vite-based setups
100
+ Dir.chdir('frontend') do
101
+ exec(env, 'npx', 'vite', '--port', @port.to_s)
102
+ end
103
+ end
104
+
105
+ def run_welcome_server
106
+ if @api_url
107
+ puts "🚀 Serving Belt welcome page on http://localhost:#{@port}"
108
+ puts " Backend API: #{@api_url}"
109
+ puts ''
110
+ puts ' Tip: Run `belt generate frontend react` to scaffold a frontend app.'
111
+ puts ''
112
+ else
113
+ puts "🚀 Serving Belt welcome page on http://localhost:#{@port}"
114
+ puts ' No deployment detected — showing pre-deploy welcome page.'
115
+ puts ' Backend is serverless — deploy with `belt deploy` to set up AWS resources.'
116
+ puts ''
117
+ puts ' Tip: Run `belt generate frontend react` to scaffold a frontend app.'
118
+ puts ''
119
+ end
120
+
121
+ require 'webrick'
122
+
123
+ server = WEBrick::HTTPServer.new(Port: @port, Logger: WEBrick::Log.new('/dev/null'), AccessLog: [])
124
+
125
+ server.mount_proc '/' do |_req, res|
126
+ res['Content-Type'] = 'text/html; charset=utf-8'
127
+ res.body = welcome_html
128
+ end
129
+
130
+ open_browser_later if @open_browser
131
+
132
+ trap('INT') { server.shutdown }
133
+ trap('TERM') { server.shutdown }
134
+
135
+ server.start
136
+ end
137
+
138
+ def gem_assets_dir
139
+ @gem_assets_dir ||= File.expand_path('../assets', __dir__)
140
+ end
141
+
142
+ def welcome_html
143
+ title = ENV.fetch('WELCOME_TITLE', 'Welcome to Belt')
144
+ subtitle = ENV.fetch('WELCOME_SUBTITLE', 'Your app is ready. Deploy to see it live on AWS.')
145
+
146
+ css = load_asset('welcome.css') || ''
147
+ image_b64 = load_image_base64('belt-default.png') || ''
148
+
149
+ if @api_url
150
+ deployed_html(css: css, image_b64: image_b64)
151
+ else
152
+ pre_deploy_html(title: title, subtitle: subtitle, css: css, image_b64: image_b64)
153
+ end
154
+ end
155
+
156
+ def load_asset(filename)
157
+ path = File.join(gem_assets_dir, filename)
158
+ File.exist?(path) ? File.read(path) : nil
159
+ end
160
+
161
+ def load_image_base64(filename)
162
+ path = File.join(gem_assets_dir, filename)
163
+ File.exist?(path) ? Base64.strict_encode64(File.binread(path)) : nil
164
+ end
165
+
166
+ def deployed_html(css:, image_b64:)
167
+ <<~HTML
168
+ <!DOCTYPE html>
169
+ <html lang="en">
170
+ <head>
171
+ <meta charset="UTF-8">
172
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
173
+ <title>#{@app_name} — Deployed</title>
174
+ <style>#{css}</style>
175
+ </head>
176
+ <body>
177
+ <div class="hero">
178
+ <img src="data:image/png;base64,#{image_b64}" alt="Belt" class="hero-bg" />
179
+ <div class="overlay">
180
+ <h1>#{@app_name}</h1>
181
+ <p class="subtitle">Deployed and running on AWS</p>
182
+ </div>
183
+ </div>
184
+ <div class="container">
185
+ <div class="next-steps">
186
+ <h2>Next Steps</h2>
187
+ <ol>
188
+ <li>View your live app: <a href="#{@api_url}">#{@api_url}</a> (#{@deploy_env || 'dev'})</li>
189
+ <li>Generate a resource: <code>belt g resource post title:string body:text</code></li>
190
+ <li>Set up a frontend: <code>belt generate frontend react</code></li>
191
+ </ol>
192
+ </div>
193
+ </div>
194
+ </body>
195
+ </html>
196
+ HTML
197
+ end
198
+
199
+ def pre_deploy_html(title:, subtitle:, css:, image_b64:)
200
+ <<~HTML
201
+ <!DOCTYPE html>
202
+ <html lang="en">
203
+ <head>
204
+ <meta charset="UTF-8">
205
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
206
+ <title>#{title}</title>
207
+ <style>#{css}</style>
208
+ </head>
209
+ <body>
210
+ <div class="hero">
211
+ <img src="data:image/png;base64,#{image_b64}" alt="Belt" class="hero-bg" />
212
+ <div class="overlay">
213
+ <h1>#{title}</h1>
214
+ <p class="subtitle">#{subtitle}</p>
215
+ </div>
216
+ </div>
217
+ <div class="container">
218
+ <div class="info">
219
+ <p><strong>App:</strong> #{@app_name}</p>
220
+ <p><strong>Status:</strong> Not yet deployed</p>
221
+ </div>
222
+ <div class="next-steps">
223
+ <h2>Next Steps</h2>
224
+ <ol>
225
+ <li>Generate a resource: <code>belt g resource post title:string body:text</code></li>
226
+ <li>Deploy to AWS: <code>belt deploy</code></li>
227
+ <li>Set up a frontend: <code>belt generate frontend react</code></li>
228
+ </ol>
229
+ </div>
230
+ </div>
231
+ </body>
232
+ </html>
233
+ HTML
234
+ end
235
+
236
+ # Reads terraform outputs to find the deployed API URL.
237
+ # Checks all available environments, preferring BELT_ENV.
238
+ def detect_api_url
239
+ infra_dir = TerraformCommand.find_infrastructure_dir
240
+ return nil unless infra_dir
241
+
242
+ # Prefer BELT_ENV, then try each environment
243
+ envs = TerraformCommand.list_environments
244
+ preferred = ENV.fetch('BELT_ENV', nil)
245
+ envs.unshift(envs.delete(preferred)) if preferred && envs.include?(preferred)
246
+
247
+ envs.each do |env|
248
+ env_dir = File.join(infra_dir, env)
249
+ next unless Dir.exist?(env_dir)
250
+ next unless File.exist?(File.join(env_dir, '.terraform'))
251
+
252
+ url = read_api_url_from_outputs(env_dir)
253
+ if url
254
+ @deploy_env = env
255
+ return url
256
+ end
257
+ end
258
+
259
+ nil
260
+ end
261
+
262
+ def read_api_url_from_outputs(env_dir)
263
+ output = nil
264
+ Dir.chdir(env_dir) do
265
+ # Use Open3 to check exit status — terraform writes errors to stdout too
266
+ require 'open3'
267
+ output, status = Open3.capture2('terraform', 'output', '-json', err: File::NULL)
268
+ return nil unless status.success?
269
+ end
270
+ return nil if output.nil? || output.strip.empty?
271
+
272
+ data = JSON.parse(output.strip)
273
+
274
+ # api_url output is a map like { "app_name" => "https://..." }
275
+ if data['api_url'] && data['api_url']['value']
276
+ value = data['api_url']['value']
277
+ case value
278
+ when String
279
+ value
280
+ when Hash
281
+ # Take the first URL from the map
282
+ value.values.first
283
+ end
284
+ end
285
+ rescue JSON::ParserError, Errno::ENOENT
286
+ nil
287
+ end
288
+
289
+ def open_browser_later
290
+ Thread.new do
291
+ sleep 1.5
292
+ url = "http://localhost:#{@port}"
293
+ if RUBY_PLATFORM.include?('darwin')
294
+ system('open', url)
295
+ elsif RUBY_PLATFORM.include?('linux')
296
+ system('xdg-open', url, out: File::NULL, err: File::NULL)
297
+ end
298
+ end
299
+ end
300
+ end
301
+ end
302
+ end
@@ -52,10 +52,18 @@ module Belt
52
52
 
53
53
  def run_state_setup
54
54
  unless aws_configured?
55
- puts ' AWS credentials not configured. Set AWS_PROFILE or configure aws sso login.'
55
+ if @aws_error&.include?('ForbiddenException') || @aws_error&.include?('AccessDenied')
56
+ puts "✗ AWS credentials found but access denied — check your profile/role configuration."
57
+ puts " #{@aws_error}"
58
+ else
59
+ puts '✗ AWS credentials not configured. Set AWS_PROFILE or configure aws sso login.'
60
+ end
56
61
  exit 1
57
62
  end
58
63
 
64
+ # Re-resolve bucket name with account ID suffix now that we have credentials
65
+ @bucket_name = resolve_bucket_name unless @custom_bucket
66
+
59
67
  @bucket_name = interactive_bucket_selection if @select_mode
60
68
  setup_or_verify_bucket
61
69
  apply_lifecycle(@bucket_name)
@@ -133,10 +141,14 @@ module Belt
133
141
  def resolve_bucket_name
134
142
  if @custom_bucket
135
143
  @custom_bucket
136
- elsif @env_name
137
- s3_safe_name("#{@app_name}-terraform-state-#{@env_name}")
138
144
  else
139
- s3_safe_name("#{@app_name}-terraform-state")
145
+ base = if @env_name
146
+ "#{@app_name}-terraform-state-#{@env_name}"
147
+ else
148
+ "#{@app_name}-terraform-state"
149
+ end
150
+ base = "#{base}-#{@aws_account_id}-#{@region}" if @aws_account_id
151
+ s3_safe_name(base)
140
152
  end
141
153
  end
142
154
 
@@ -184,11 +196,33 @@ module Belt
184
196
  # --- AWS operations ---
185
197
 
186
198
  def aws_configured?
187
- system('aws', 'sts', 'get-caller-identity', out: File::NULL, err: File::NULL)
199
+ output, status = Open3.capture2e('aws', 'sts', 'get-caller-identity')
200
+ if status.success?
201
+ data = JSON.parse(output) rescue {}
202
+ @aws_account_id = data['Account']
203
+ true
204
+ else
205
+ @aws_error = output.strip
206
+ false
207
+ end
208
+ end
209
+
210
+ def aws_account_id
211
+ @aws_account_id
188
212
  end
189
213
 
190
214
  def bucket_exists?(bucket)
191
- system('aws', 's3api', 'head-bucket', '--bucket', bucket, err: File::NULL)
215
+ output, status = Open3.capture2e('aws', 's3api', 'head-bucket', '--bucket', bucket)
216
+ return true if status.success?
217
+
218
+ # 403 = bucket exists but owned by someone else; 404 = doesn't exist
219
+ if output.include?('403') || output.include?('Forbidden')
220
+ puts "\n✗ Bucket '#{bucket}' exists but is owned by a different AWS account."
221
+ puts ' S3 bucket names are globally unique. Choose a different name with --bucket.'
222
+ exit 1
223
+ end
224
+
225
+ false
192
226
  end
193
227
 
194
228
  def create_bucket(bucket)
data/lib/belt/cli.rb CHANGED
@@ -11,6 +11,8 @@ require_relative 'cli/frontend_deploy_command'
11
11
  require_relative 'cli/views_command'
12
12
  require_relative 'cli/setup_command'
13
13
  require_relative 'cli/terraform_command'
14
+ require_relative 'cli/deploy_command'
15
+ require_relative 'cli/server_command'
14
16
  require_relative 'cli/routes_command'
15
17
  require_relative 'cli/tasks_command'
16
18
  require_relative 'cli/console_command'
@@ -24,15 +26,8 @@ module Belt
24
26
  %w[console c] => Belt::CLI::ConsoleCommand,
25
27
  %w[tasks --tasks -T] => Belt::CLI::TasksCommand,
26
28
  'setup' => Belt::CLI::SetupCommand,
27
- 'deploy' => lambda { |args|
28
- subcommand = args.shift
29
- if subcommand == 'frontend'
30
- Belt::CLI::FrontendDeployCommand.run(args)
31
- else
32
- puts 'Usage: belt deploy frontend <environment>'
33
- exit 1
34
- end
35
- },
29
+ 'deploy' => Belt::CLI::DeployCommand,
30
+ %w[server s] => Belt::CLI::ServerCommand,
36
31
  %w[version --version -v] => ->(_args) { puts "Belt #{Belt::VERSION}" }
37
32
  }.freeze
38
33
 
@@ -80,6 +75,10 @@ module Belt
80
75
  generate frontend <react|vue|svelte> Scaffold a frontend app
81
76
  generate views <resource> [fields...] Generate React pages for REST actions
82
77
  generate environment <name> Create a new environment
78
+ server Start local dev server (frontend)
79
+ s Alias for server
80
+ deploy [environment] Deploy to AWS (init → plan → apply)
81
+ deploy frontend <env> Build and deploy frontend to AWS
83
82
  routes [-g PATTERN] [-f json] Show route definitions
84
83
  console Start an interactive console (IRB)
85
84
  c Alias for console
@@ -88,7 +87,6 @@ module Belt
88
87
  setup state Create/select S3 state bucket
89
88
  setup tables <env> Generate DynamoDB tables from schema
90
89
  setup frontend <env> Generate S3 + CloudFront infrastructure
91
- deploy frontend <env> Build and deploy frontend to AWS
92
90
  init [environment] <env> terraform init for environment
93
91
  plan [environment] <env> terraform plan for environment
94
92
  apply [environment] <env> terraform apply for environment
@@ -110,8 +108,11 @@ module Belt
110
108
  belt new blog --frontend react
111
109
  belt generate resource post title:string content:text status:string
112
110
  belt generate frontend react
113
- belt setup frontend wups
111
+ belt server # Start local frontend server
112
+ belt deploy # Deploy dev to AWS
113
+ belt deploy prod --auto # Deploy prod without confirmation
114
114
  belt deploy frontend wups
115
+ belt setup frontend wups
115
116
  belt apply wups
116
117
  belt tasks # list all rake tasks
117
118
  belt lambda:build_layer # run a rake task directly