belt 0.0.7 → 0.1.1
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
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +29 -1
- data/README.md +150 -51
- data/exe/belt +6 -0
- data/lib/belt/action_router.rb +7 -1
- data/lib/belt/cli/app_detection.rb +16 -0
- data/lib/belt/cli/bucket_security.rb +122 -0
- data/lib/belt/cli/env_resolver.rb +15 -0
- data/lib/belt/cli/environment_command.rb +77 -0
- data/lib/belt/cli/frontend_command.rb +85 -0
- data/lib/belt/cli/frontend_deploy_command.rb +125 -0
- data/lib/belt/cli/frontend_setup_command.rb +64 -0
- data/lib/belt/cli/generate_command.rb +206 -0
- data/lib/belt/cli/new_command.rb +126 -0
- data/lib/belt/cli/routes_command/route_inference.rb +100 -0
- data/lib/belt/cli/routes_command/schema_loader.rb +71 -0
- data/lib/belt/cli/routes_command.rb +307 -0
- data/lib/belt/cli/setup_command.rb +261 -0
- data/lib/belt/cli/tables_command.rb +138 -0
- data/lib/belt/cli/tasks_command.rb +110 -0
- data/lib/belt/cli/terraform_command.rb +77 -0
- data/lib/belt/cli/views_command.rb +134 -0
- data/lib/belt/cli.rb +117 -0
- data/lib/belt/lambda_handler.rb +16 -0
- data/lib/belt/root.rb +26 -0
- data/lib/belt/route_dsl.rb +605 -0
- data/lib/belt/table_inference.rb +71 -0
- data/lib/belt/version.rb +1 -1
- data/lib/belt.rb +1 -0
- data/lib/templates/environment/backend.tf.erb +8 -0
- data/lib/templates/environment/main.tf.erb +42 -0
- data/lib/templates/environment/terraform.tfvars.erb +1 -0
- data/lib/templates/environment/variables.tf.erb +16 -0
- data/lib/templates/frontend/react/index.html.erb +12 -0
- data/lib/templates/frontend/react/package.json.erb +20 -0
- data/lib/templates/frontend/react/src/App.jsx +14 -0
- data/lib/templates/frontend/react/src/index.css +10 -0
- data/lib/templates/frontend/react/src/lib/apiClient.js.erb +19 -0
- data/lib/templates/frontend/react/src/main.jsx +10 -0
- data/lib/templates/frontend/react/src/pages/Home.jsx.erb +10 -0
- data/lib/templates/frontend/react/vite.config.js +8 -0
- data/lib/templates/frontend_infra/frontend.tf.erb +159 -0
- data/lib/templates/generate/controller.rb.erb +59 -0
- data/lib/templates/generate/model.rb.erb +20 -0
- data/lib/templates/new_app/AGENTS.md.erb +130 -0
- data/lib/templates/new_app/Gemfile.erb +5 -0
- data/lib/templates/new_app/README.md.erb +25 -0
- data/lib/templates/new_app/Rakefile.erb +12 -0
- data/lib/templates/new_app/gitignore.erb +14 -0
- data/lib/templates/new_app/infrastructure/routes.tf.rb.erb +5 -0
- data/lib/templates/new_app/infrastructure/schema.tf.rb.erb +9 -0
- data/lib/templates/new_app/lambda/Gemfile.erb +7 -0
- data/lib/templates/new_app/lambda/api.rb.erb +22 -0
- data/lib/templates/new_app/lambda/controllers/application_controller.rb.erb +6 -0
- data/lib/templates/new_app/lambda/lib/routes/routes.rb.erb +11 -0
- data/lib/templates/new_app/lambda/models/application_record.rb.erb +6 -0
- data/lib/templates/new_app/lambda/models/concerns/timestampable.rb.erb +23 -0
- data/lib/templates/views/Edit.jsx.erb +38 -0
- data/lib/templates/views/Form.jsx.erb +34 -0
- data/lib/templates/views/Index.jsx.erb +39 -0
- data/lib/templates/views/New.jsx.erb +26 -0
- data/lib/templates/views/Show.jsx.erb +46 -0
- data.tar.gz.sig +0 -0
- metadata +73 -3
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
require 'optparse'
|
|
5
|
+
|
|
6
|
+
module Belt
|
|
7
|
+
module CLI
|
|
8
|
+
class TasksCommand
|
|
9
|
+
def self.run(args)
|
|
10
|
+
new(args).run
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Check if a given command name looks like a rake task that exists
|
|
14
|
+
def self.rake_task?(name)
|
|
15
|
+
return false unless name.include?(':') || name.match?(/\A[a-z_]+\z/)
|
|
16
|
+
return false unless File.exist?('Rakefile') || File.exist?('rakefile') || File.exist?('Rakefile.rb')
|
|
17
|
+
|
|
18
|
+
# Only treat colon-namespaced commands as potential rake tasks to avoid
|
|
19
|
+
# ambiguity with belt's own commands
|
|
20
|
+
name.include?(':')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Invoke a specific rake task by name
|
|
24
|
+
def self.invoke(task_name, args)
|
|
25
|
+
unless File.exist?('Rakefile') || File.exist?('rakefile') || File.exist?('Rakefile.rb')
|
|
26
|
+
abort "Error: No Rakefile found. Cannot run task '#{task_name}'."
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
cmd = ['bundle', 'exec', 'rake', task_name] + args
|
|
30
|
+
exec(*cmd)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def initialize(args)
|
|
34
|
+
@options = {}
|
|
35
|
+
parse_options(args)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def run
|
|
39
|
+
abort 'Error: No Rakefile found. Add a Rakefile to your project to discover tasks.' unless rakefile_available?
|
|
40
|
+
|
|
41
|
+
tasks = load_tasks
|
|
42
|
+
tasks = apply_grep(tasks) if @options[:grep]
|
|
43
|
+
|
|
44
|
+
if tasks.empty?
|
|
45
|
+
puts 'No rake tasks found.'
|
|
46
|
+
else
|
|
47
|
+
output_tasks(tasks)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def parse_options(args)
|
|
54
|
+
OptionParser.new do |opts|
|
|
55
|
+
opts.banner = 'Usage: belt tasks [options]'
|
|
56
|
+
|
|
57
|
+
opts.on('-g', '--grep PATTERN', 'Filter tasks matching pattern') do |pattern|
|
|
58
|
+
@options[:grep] = pattern
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
opts.on('-a', '--all', 'Show all tasks (including those without descriptions)') do
|
|
62
|
+
@options[:all] = true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
opts.on('-h', '--help', 'Show this help') do
|
|
66
|
+
puts opts
|
|
67
|
+
exit
|
|
68
|
+
end
|
|
69
|
+
end.parse!(args)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def rakefile_available?
|
|
73
|
+
File.exist?('Rakefile') || File.exist?('rakefile') || File.exist?('Rakefile.rb')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def load_tasks
|
|
77
|
+
cmd = @options[:all] ? %w[bundle exec rake -T -A] : %w[bundle exec rake -T]
|
|
78
|
+
output, status = Open3.capture2(*cmd, err: File::NULL)
|
|
79
|
+
|
|
80
|
+
abort 'Error: Failed to load rake tasks. Ensure `bundle install` has been run.' unless status.success?
|
|
81
|
+
|
|
82
|
+
parse_task_output(output)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def parse_task_output(output)
|
|
86
|
+
output.lines.filter_map do |line|
|
|
87
|
+
match = line.match(/^rake\s+(\S+)\s*#\s*(.*)$/)
|
|
88
|
+
next unless match
|
|
89
|
+
|
|
90
|
+
{ name: match[1], description: match[2].strip }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def apply_grep(tasks)
|
|
95
|
+
pattern = Regexp.new(@options[:grep], Regexp::IGNORECASE)
|
|
96
|
+
tasks.select do |t|
|
|
97
|
+
t[:name].match?(pattern) || t[:description].match?(pattern)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def output_tasks(tasks)
|
|
102
|
+
name_w = [tasks.map { |t| t[:name].length }.max, 4].max
|
|
103
|
+
|
|
104
|
+
tasks.each do |t|
|
|
105
|
+
puts "belt #{t[:name].ljust(name_w)} # #{t[:description]}"
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
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,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'version'
|
|
4
|
+
require_relative 'root'
|
|
5
|
+
require_relative 'cli/env_resolver'
|
|
6
|
+
require_relative 'cli/new_command'
|
|
7
|
+
require_relative 'cli/generate_command'
|
|
8
|
+
require_relative 'cli/frontend_command'
|
|
9
|
+
require_relative 'cli/frontend_setup_command'
|
|
10
|
+
require_relative 'cli/frontend_deploy_command'
|
|
11
|
+
require_relative 'cli/views_command'
|
|
12
|
+
require_relative 'cli/setup_command'
|
|
13
|
+
require_relative 'cli/terraform_command'
|
|
14
|
+
require_relative 'cli/routes_command'
|
|
15
|
+
require_relative 'cli/tasks_command'
|
|
16
|
+
|
|
17
|
+
module Belt
|
|
18
|
+
module CLI
|
|
19
|
+
COMMANDS_DEFINITION = {
|
|
20
|
+
'new' => Belt::CLI::NewCommand,
|
|
21
|
+
%w[generate g] => Belt::CLI::GenerateCommand,
|
|
22
|
+
'routes' => Belt::CLI::RoutesCommand,
|
|
23
|
+
%w[tasks --tasks -T] => Belt::CLI::TasksCommand,
|
|
24
|
+
'setup' => Belt::CLI::SetupCommand,
|
|
25
|
+
'deploy' => lambda { |args|
|
|
26
|
+
subcommand = args.shift
|
|
27
|
+
if subcommand == 'frontend'
|
|
28
|
+
Belt::CLI::FrontendDeployCommand.run(args)
|
|
29
|
+
else
|
|
30
|
+
puts 'Usage: belt deploy frontend <environment>'
|
|
31
|
+
exit 1
|
|
32
|
+
end
|
|
33
|
+
},
|
|
34
|
+
%w[version --version -v] => ->(_args) { puts "Belt #{Belt::VERSION}" }
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
COMMANDS = COMMANDS_DEFINITION.each_with_object({}) do |(keys, handler), hash|
|
|
38
|
+
Array(keys).each { |key| hash[key] = handler }
|
|
39
|
+
end.freeze
|
|
40
|
+
|
|
41
|
+
TERRAFORM_ACTIONS = Belt::CLI::TerraformCommand::ACTIONS
|
|
42
|
+
|
|
43
|
+
def self.start(args)
|
|
44
|
+
command = args.shift
|
|
45
|
+
|
|
46
|
+
if command.nil?
|
|
47
|
+
puts usage
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Terraform shorthand: belt init wups, belt plan wups, belt apply wups
|
|
52
|
+
return Belt::CLI::TerraformCommand.run(command, args) if TERRAFORM_ACTIONS.include?(command)
|
|
53
|
+
|
|
54
|
+
handler = COMMANDS[command]
|
|
55
|
+
|
|
56
|
+
# If no built-in command matched, try running it as a rake task
|
|
57
|
+
if handler.nil?
|
|
58
|
+
return Belt::CLI::TasksCommand.invoke(command, args) if Belt::CLI::TasksCommand.rake_task?(command)
|
|
59
|
+
|
|
60
|
+
puts "Unknown command: #{command}\n\n#{usage}"
|
|
61
|
+
exit 1
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
if handler.is_a?(Proc)
|
|
65
|
+
handler.call(args)
|
|
66
|
+
else
|
|
67
|
+
handler.run(args)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.usage
|
|
72
|
+
<<~USAGE
|
|
73
|
+
Usage: belt <command> [options]
|
|
74
|
+
|
|
75
|
+
Commands:
|
|
76
|
+
new <app_name> [--frontend react] Create a new Belt application
|
|
77
|
+
generate <resource|model|controller> <name> Generate components
|
|
78
|
+
generate frontend <react|vue|svelte> Scaffold a frontend app
|
|
79
|
+
generate views <resource> [fields...] Generate React pages for REST actions
|
|
80
|
+
generate environment <name> Create a new environment
|
|
81
|
+
routes [-g PATTERN] [-f json] Show route definitions
|
|
82
|
+
tasks [-g PATTERN] [-a] List available rake tasks
|
|
83
|
+
-T [-g PATTERN] [-a] Alias for tasks
|
|
84
|
+
setup state Create/select S3 state bucket
|
|
85
|
+
setup tables <env> Generate DynamoDB tables from schema
|
|
86
|
+
setup frontend <env> Generate S3 + CloudFront infrastructure
|
|
87
|
+
deploy frontend <env> Build and deploy frontend to AWS
|
|
88
|
+
init <env> terraform init for environment
|
|
89
|
+
plan <env> terraform plan for environment
|
|
90
|
+
apply <env> terraform apply for environment
|
|
91
|
+
destroy <env> terraform destroy for environment
|
|
92
|
+
output <env> terraform output for environment
|
|
93
|
+
--version Show Belt version
|
|
94
|
+
|
|
95
|
+
Rake Tasks:
|
|
96
|
+
Any rake task from your Gemfile dependencies can be run directly:
|
|
97
|
+
belt lambda:build_layer Run a rake task by name
|
|
98
|
+
|
|
99
|
+
Environment:
|
|
100
|
+
Set BELT_ENV to skip the <env> argument:
|
|
101
|
+
export BELT_ENV=wups
|
|
102
|
+
belt apply # uses BELT_ENV
|
|
103
|
+
belt apply dev01 # explicit arg wins
|
|
104
|
+
|
|
105
|
+
Examples:
|
|
106
|
+
belt new blog --frontend react
|
|
107
|
+
belt generate resource post title:string content:text status:string
|
|
108
|
+
belt generate frontend react
|
|
109
|
+
belt setup frontend wups
|
|
110
|
+
belt deploy frontend wups
|
|
111
|
+
belt apply wups
|
|
112
|
+
belt tasks # list all rake tasks
|
|
113
|
+
belt lambda:build_layer # run a rake task directly
|
|
114
|
+
USAGE
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
data/lib/belt/lambda_handler.rb
CHANGED
|
@@ -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/root.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Belt
|
|
4
|
+
def self.root
|
|
5
|
+
@root ||= detect_root
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def self.root=(path)
|
|
9
|
+
@root = path
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.detect_root
|
|
13
|
+
dir = Dir.pwd
|
|
14
|
+
loop do
|
|
15
|
+
return dir if File.exist?(File.join(dir, 'infrastructure/routes.tf.rb'))
|
|
16
|
+
|
|
17
|
+
parent = File.dirname(dir)
|
|
18
|
+
break if parent == dir
|
|
19
|
+
|
|
20
|
+
dir = parent
|
|
21
|
+
end
|
|
22
|
+
Dir.pwd
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private_class_method :detect_root
|
|
26
|
+
end
|