belt 0.1.0 → 0.1.2
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 +169 -51
- data/lib/belt/cli/console_command.rb +128 -0
- data/lib/belt/cli/new_command.rb +3 -1
- data/lib/belt/cli/routes_command/route_inference.rb +94 -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/tables_command.rb +2 -2
- data/lib/belt/cli/tasks_command.rb +110 -0
- data/lib/belt/cli.rb +28 -5
- 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/new_app/AGENTS.md.erb +1 -1
- data/lib/templates/new_app/Gemfile.erb +1 -0
- data/lib/templates/new_app/Rakefile.erb +12 -0
- data/lib/templates/new_app/gitignore.erb +1 -1
- data/lib/templates/new_app/infrastructure/routes.tf.rb.erb +1 -1
- data/lib/templates/new_app/infrastructure/schema.tf.rb.erb +1 -1
- data/lib/templates/new_app/lambda/config/environment.rb.erb +18 -0
- data.tar.gz.sig +0 -0
- metadata +25 -2
- metadata.gz.sig +0 -0
- data/lib/templates/new_app/lambda/Gemfile.erb +0 -7
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'optparse'
|
|
5
|
+
require 'fileutils'
|
|
6
|
+
require_relative '../route_dsl'
|
|
7
|
+
require_relative '../table_inference'
|
|
8
|
+
require_relative 'routes_command/schema_loader'
|
|
9
|
+
require_relative 'routes_command/route_inference'
|
|
10
|
+
|
|
11
|
+
module Belt
|
|
12
|
+
module CLI
|
|
13
|
+
class RoutesCommand
|
|
14
|
+
include SchemaLoader
|
|
15
|
+
include RouteInference
|
|
16
|
+
|
|
17
|
+
def self.run(args)
|
|
18
|
+
new(args).run
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(args)
|
|
22
|
+
@options = {}
|
|
23
|
+
parse_options(args)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def run
|
|
27
|
+
routes_file = find_routes_file
|
|
28
|
+
abort 'Error: No routes file found. Expected infrastructure/routes.tf.rb' unless routes_file
|
|
29
|
+
|
|
30
|
+
dsl = load_routes(routes_file)
|
|
31
|
+
@table_inference = TableInference.new(@options[:tables_file])
|
|
32
|
+
routes = collect_routes(dsl)
|
|
33
|
+
routes = apply_grep(routes) if @options[:grep]
|
|
34
|
+
|
|
35
|
+
warn 'Warning: --output-dir has no effect without --namespace' if @options[:output_dir] && !@options[:namespace]
|
|
36
|
+
|
|
37
|
+
if @options[:namespace]
|
|
38
|
+
output_ruby(routes, @options[:namespace], routes_file)
|
|
39
|
+
elsif @options[:format] == 'json'
|
|
40
|
+
output = { routes: routes }
|
|
41
|
+
models = load_schema_models(routes_file)
|
|
42
|
+
output[:models] = models if models.any?
|
|
43
|
+
puts JSON.pretty_generate(output)
|
|
44
|
+
else
|
|
45
|
+
output_concise(routes)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def parse_options(args)
|
|
52
|
+
OptionParser.new do |opts|
|
|
53
|
+
opts.banner = 'Usage: belt routes [options]'
|
|
54
|
+
|
|
55
|
+
opts.on('-g', '--grep PATTERN', 'Filter routes matching pattern') do |pattern|
|
|
56
|
+
@options[:grep] = pattern
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
opts.on('-f', '--format FORMAT', 'Output format: concise (default), json') do |format|
|
|
60
|
+
@options[:format] = format
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
opts.on('--namespace NAMESPACE', 'Generate Ruby route files for NAMESPACE (or "all")') do |ns|
|
|
64
|
+
@options[:namespace] = ns
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
opts.on('--output-dir DIR', 'Output directory for generated files') do |dir|
|
|
68
|
+
@options[:output_dir] = dir
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
opts.on('--schema FILE', 'Path to schema.tf.rb for model definitions') do |file|
|
|
72
|
+
@options[:schema_file] = file
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
opts.on('--tables-file FILE', 'Path to Terraform file with DynamoDB table definitions') do |file|
|
|
76
|
+
@options[:tables_file] = file
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
opts.on('-h', '--help', 'Show this help') do
|
|
80
|
+
puts opts
|
|
81
|
+
exit
|
|
82
|
+
end
|
|
83
|
+
end.parse!(args)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def find_routes_file
|
|
87
|
+
path = 'infrastructure/routes.tf.rb'
|
|
88
|
+
File.exist?(path) ? path : nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def load_routes(file)
|
|
92
|
+
# Reset schema builder for clean state
|
|
93
|
+
Belt.instance_variable_set(:@application, nil)
|
|
94
|
+
|
|
95
|
+
content = File.read(file)
|
|
96
|
+
if content.include?('Belt.application.routes.draw')
|
|
97
|
+
binding_context = binding
|
|
98
|
+
eval(content, binding_context, file) # rubocop:disable Security/Eval
|
|
99
|
+
else
|
|
100
|
+
Belt::RouteDSL.load_from_file(file)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def collect_routes(dsl)
|
|
105
|
+
routes = []
|
|
106
|
+
dsl.api_gateways.each do |gateway|
|
|
107
|
+
gateway.routes.each do |route|
|
|
108
|
+
routes << build_route_hash(route, gateway)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
routes.sort_by { |r| route_specificity(r[:path], r[:verb]) }
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def build_route_hash(route, gateway)
|
|
115
|
+
hash = {
|
|
116
|
+
name: extract_route_name(route.path),
|
|
117
|
+
verb: route.method,
|
|
118
|
+
path: normalize_path(route.path),
|
|
119
|
+
gateway: gateway.name,
|
|
120
|
+
lambda: route.lambda.to_s,
|
|
121
|
+
controller: infer_controller(route, gateway),
|
|
122
|
+
action: infer_action(route, gateway),
|
|
123
|
+
auth: route.auth.to_s,
|
|
124
|
+
tables: get_route_tables(route),
|
|
125
|
+
request_model: route.request_model.to_s,
|
|
126
|
+
response_model: route.response_model.to_s
|
|
127
|
+
}
|
|
128
|
+
rc = route.response_context.to_s
|
|
129
|
+
hash[:response_context] = rc unless rc.empty?
|
|
130
|
+
hash
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def get_route_tables(route)
|
|
134
|
+
if route.tables.any?
|
|
135
|
+
route.tables.map(&:to_s)
|
|
136
|
+
else
|
|
137
|
+
@table_inference.infer_tables_from_route(route)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def extract_route_name(path)
|
|
142
|
+
segments = path.split('/').reject(&:empty?)
|
|
143
|
+
return 'root' if segments.empty?
|
|
144
|
+
|
|
145
|
+
segments.reject { |s| s.start_with?('{', ':') }
|
|
146
|
+
.map { |s| s.gsub('-', '_') }
|
|
147
|
+
.join('_')
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def output_ruby(routes, namespace, _routes_file)
|
|
151
|
+
output_dir = @options[:output_dir] || File.join(Belt.root, 'lambda/lib/routes')
|
|
152
|
+
FileUtils.mkdir_p(output_dir)
|
|
153
|
+
puts "Writing to #{output_dir}/:"
|
|
154
|
+
|
|
155
|
+
if namespace == 'all'
|
|
156
|
+
generate_all_manifests(routes, output_dir)
|
|
157
|
+
else
|
|
158
|
+
# Generate gateway-based manifest (all routes for this gateway)
|
|
159
|
+
filtered = routes.select { |r| r[:gateway] == namespace }
|
|
160
|
+
# Fall back to lambda-based if no gateway match
|
|
161
|
+
filtered = routes.select { |r| r[:lambda] == namespace } if filtered.empty?
|
|
162
|
+
if filtered.empty?
|
|
163
|
+
warn "No routes found for namespace '#{namespace}' - skipping"
|
|
164
|
+
return
|
|
165
|
+
end
|
|
166
|
+
write_ruby_manifest(filtered, namespace, output_dir)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def generate_all_manifests(routes, output_dir)
|
|
171
|
+
# Gateway-based manifests (primary — used by main Lambda entry points)
|
|
172
|
+
by_gateway = routes.group_by { |r| r[:gateway] }
|
|
173
|
+
by_gateway.each { |gw, gw_routes| write_ruby_manifest(gw_routes, gw, output_dir) }
|
|
174
|
+
|
|
175
|
+
# Scoped lambda manifests (where lambda != gateway — separate Lambda functions)
|
|
176
|
+
scoped = routes.reject { |r| r[:lambda] == r[:gateway] }
|
|
177
|
+
by_lambda = scoped.group_by { |r| r[:lambda] }
|
|
178
|
+
by_lambda.each { |lam, lam_routes| write_ruby_manifest(lam_routes, lam, output_dir) }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def write_ruby_manifest(routes, name, output_dir)
|
|
182
|
+
output_file = File.join(output_dir, "#{name}_routes.rb")
|
|
183
|
+
content = generate_ruby_content(routes, name)
|
|
184
|
+
File.write(output_file, content)
|
|
185
|
+
puts " ✅ #{name}_routes.rb (#{routes.length} routes)"
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def generate_ruby_content(routes, namespace)
|
|
189
|
+
constant_name = namespace.upcase
|
|
190
|
+
lines = [
|
|
191
|
+
'# frozen_string_literal: true',
|
|
192
|
+
'',
|
|
193
|
+
"# Auto-generated by: belt routes --namespace #{namespace}",
|
|
194
|
+
'# Do not edit manually',
|
|
195
|
+
'',
|
|
196
|
+
'module Routes',
|
|
197
|
+
" #{constant_name} = ["
|
|
198
|
+
]
|
|
199
|
+
|
|
200
|
+
routes.each_with_index do |route, index|
|
|
201
|
+
lines << ' {'
|
|
202
|
+
lines << " verb: #{route[:verb].inspect},"
|
|
203
|
+
lines << " path: #{route[:path].inspect},"
|
|
204
|
+
lines << " gateway: #{route[:gateway].inspect},"
|
|
205
|
+
lines << " lambda: #{route[:lambda].inspect},"
|
|
206
|
+
lines << " controller: #{route[:controller].inspect},"
|
|
207
|
+
lines << " action: #{route[:action].inspect},"
|
|
208
|
+
lines << " auth: #{route[:auth].inspect},"
|
|
209
|
+
tables_syms = route[:tables].map { |t| ":#{t}" }.join(', ')
|
|
210
|
+
lines << " tables: [#{tables_syms}]"
|
|
211
|
+
lines << " }#{',' if index < routes.length - 1}"
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
lines << ' ].freeze'
|
|
215
|
+
lines << 'end'
|
|
216
|
+
lines << ''
|
|
217
|
+
lines.join("\n")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def normalize_path(path)
|
|
221
|
+
path = "/#{path}" unless path.start_with?('/')
|
|
222
|
+
normalized = path.gsub(%r{/([a-zA-Z_][a-zA-Z0-9_]*?)/:id(/|$)}) do
|
|
223
|
+
resource = ::Regexp.last_match(1)
|
|
224
|
+
trailing = ::Regexp.last_match(2)
|
|
225
|
+
singular = singularize(resource)
|
|
226
|
+
"/#{resource}/{#{singular}_id}#{trailing}"
|
|
227
|
+
end
|
|
228
|
+
normalized.gsub(/:([a-zA-Z_][a-zA-Z0-9_]*)/) { "{#{::Regexp.last_match(1)}}" }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def apply_grep(routes)
|
|
232
|
+
pattern = Regexp.new(@options[:grep], Regexp::IGNORECASE)
|
|
233
|
+
routes.select do |r|
|
|
234
|
+
r[:path].match?(pattern) ||
|
|
235
|
+
r[:gateway].to_s.match?(pattern) ||
|
|
236
|
+
r[:lambda].match?(pattern) ||
|
|
237
|
+
r[:verb].match?(pattern) ||
|
|
238
|
+
r[:controller].match?(pattern) ||
|
|
239
|
+
r[:action].match?(pattern)
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def output_concise(routes)
|
|
244
|
+
return puts('No routes defined.') if routes.empty?
|
|
245
|
+
|
|
246
|
+
multi_gateway = routes.map { |r| r[:gateway] }.uniq.length > 1
|
|
247
|
+
verb_w = [routes.map { |r| r[:verb].length }.max, 6].max
|
|
248
|
+
path_w = [routes.map { |r| r[:path].length }.max, 4].max
|
|
249
|
+
|
|
250
|
+
if multi_gateway
|
|
251
|
+
output_concise_multi_gateway(routes, verb_w, path_w)
|
|
252
|
+
else
|
|
253
|
+
output_concise_single_gateway(routes, verb_w, path_w)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def output_concise_multi_gateway(routes, verb_w, path_w)
|
|
258
|
+
gw_w = [routes.map { |r| r[:gateway].to_s.length }.max, 7].max
|
|
259
|
+
lam_w = [routes.map { |r| r[:lambda].length }.max, 6].max
|
|
260
|
+
|
|
261
|
+
header = "#{'VERB'.ljust(verb_w)} #{'PATH'.ljust(path_w)} " \
|
|
262
|
+
"#{'GATEWAY'.ljust(gw_w)} #{'LAMBDA'.ljust(lam_w)} CONTROLLER#ACTION"
|
|
263
|
+
puts header
|
|
264
|
+
puts '-' * (verb_w + path_w + gw_w + lam_w + 20)
|
|
265
|
+
|
|
266
|
+
routes.each do |r|
|
|
267
|
+
line = "#{r[:verb].ljust(verb_w)} #{r[:path].ljust(path_w)} " \
|
|
268
|
+
"#{r[:gateway].to_s.ljust(gw_w)} #{r[:lambda].ljust(lam_w)} " \
|
|
269
|
+
"#{r[:controller]}##{r[:action]}"
|
|
270
|
+
puts line
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def output_concise_single_gateway(routes, verb_w, path_w)
|
|
275
|
+
puts "#{'VERB'.ljust(verb_w)} #{'PATH'.ljust(path_w)} CONTROLLER#ACTION"
|
|
276
|
+
puts '-' * (verb_w + path_w + 30)
|
|
277
|
+
|
|
278
|
+
routes.each do |r|
|
|
279
|
+
puts "#{r[:verb].ljust(verb_w)} #{r[:path].ljust(path_w)} #{r[:controller]}##{r[:action]}"
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def route_specificity(path, verb)
|
|
284
|
+
segments = path.split('/').reject(&:empty?)
|
|
285
|
+
param_count = segments.count { |s| s.start_with?('{') }
|
|
286
|
+
segment_count = segments.length
|
|
287
|
+
[param_count, -segment_count, path, verb_order(verb)]
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def verb_order(verb)
|
|
291
|
+
{ 'GET' => 0, 'POST' => 1, 'PUT' => 2, 'PATCH' => 3, 'DELETE' => 4 }[verb] || 99
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def singularize(word)
|
|
295
|
+
if word.end_with?('ies')
|
|
296
|
+
"#{word[0..-4]}y"
|
|
297
|
+
elsif word.end_with?('ses') || word.end_with?('xes') || word.end_with?('zes')
|
|
298
|
+
word[0..-3]
|
|
299
|
+
elsif word.end_with?('s') && !word.end_with?('ss')
|
|
300
|
+
word[0..-2]
|
|
301
|
+
else
|
|
302
|
+
word
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
end
|
|
@@ -59,8 +59,8 @@ module Belt
|
|
|
59
59
|
schema_content = File.read(SCHEMA_FILE)
|
|
60
60
|
|
|
61
61
|
# Replace DSL wrapper with direct parser call
|
|
62
|
-
#
|
|
63
|
-
inner = schema_content.sub(/\
|
|
62
|
+
# Belt.application.schema.draw do ... end → parser.instance_eval do ... end
|
|
63
|
+
inner = schema_content.sub(/\A(?:Belt\.application)\.schema\.draw do\n?/, '').sub(/\nend\s*\z/, '')
|
|
64
64
|
parser.instance_eval(inner, SCHEMA_FILE)
|
|
65
65
|
parser.models
|
|
66
66
|
end
|
|
@@ -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
|
data/lib/belt/cli.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'version'
|
|
4
|
+
require_relative 'root'
|
|
4
5
|
require_relative 'cli/env_resolver'
|
|
5
6
|
require_relative 'cli/new_command'
|
|
6
7
|
require_relative 'cli/generate_command'
|
|
@@ -10,13 +11,18 @@ require_relative 'cli/frontend_deploy_command'
|
|
|
10
11
|
require_relative 'cli/views_command'
|
|
11
12
|
require_relative 'cli/setup_command'
|
|
12
13
|
require_relative 'cli/terraform_command'
|
|
14
|
+
require_relative 'cli/routes_command'
|
|
15
|
+
require_relative 'cli/tasks_command'
|
|
16
|
+
require_relative 'cli/console_command'
|
|
13
17
|
|
|
14
18
|
module Belt
|
|
15
19
|
module CLI
|
|
16
|
-
|
|
20
|
+
COMMANDS_DEFINITION = {
|
|
17
21
|
'new' => Belt::CLI::NewCommand,
|
|
18
|
-
|
|
19
|
-
'
|
|
22
|
+
%w[generate g] => Belt::CLI::GenerateCommand,
|
|
23
|
+
'routes' => Belt::CLI::RoutesCommand,
|
|
24
|
+
%w[console c] => Belt::CLI::ConsoleCommand,
|
|
25
|
+
%w[tasks --tasks -T] => Belt::CLI::TasksCommand,
|
|
20
26
|
'setup' => Belt::CLI::SetupCommand,
|
|
21
27
|
'deploy' => lambda { |args|
|
|
22
28
|
subcommand = args.shift
|
|
@@ -27,10 +33,13 @@ module Belt
|
|
|
27
33
|
exit 1
|
|
28
34
|
end
|
|
29
35
|
},
|
|
30
|
-
|
|
31
|
-
'-v' => ->(_args) { puts "Belt #{Belt::VERSION}" }
|
|
36
|
+
%w[version --version -v] => ->(_args) { puts "Belt #{Belt::VERSION}" }
|
|
32
37
|
}.freeze
|
|
33
38
|
|
|
39
|
+
COMMANDS = COMMANDS_DEFINITION.each_with_object({}) do |(keys, handler), hash|
|
|
40
|
+
Array(keys).each { |key| hash[key] = handler }
|
|
41
|
+
end.freeze
|
|
42
|
+
|
|
34
43
|
TERRAFORM_ACTIONS = Belt::CLI::TerraformCommand::ACTIONS
|
|
35
44
|
|
|
36
45
|
def self.start(args)
|
|
@@ -46,7 +55,10 @@ module Belt
|
|
|
46
55
|
|
|
47
56
|
handler = COMMANDS[command]
|
|
48
57
|
|
|
58
|
+
# If no built-in command matched, try running it as a rake task
|
|
49
59
|
if handler.nil?
|
|
60
|
+
return Belt::CLI::TasksCommand.invoke(command, args) if Belt::CLI::TasksCommand.rake_task?(command)
|
|
61
|
+
|
|
50
62
|
puts "Unknown command: #{command}\n\n#{usage}"
|
|
51
63
|
exit 1
|
|
52
64
|
end
|
|
@@ -68,6 +80,11 @@ module Belt
|
|
|
68
80
|
generate frontend <react|vue|svelte> Scaffold a frontend app
|
|
69
81
|
generate views <resource> [fields...] Generate React pages for REST actions
|
|
70
82
|
generate environment <name> Create a new environment
|
|
83
|
+
routes [-g PATTERN] [-f json] Show route definitions
|
|
84
|
+
console Start an interactive console (IRB)
|
|
85
|
+
c Alias for console
|
|
86
|
+
tasks [-g PATTERN] [-a] List available rake tasks
|
|
87
|
+
-T [-g PATTERN] [-a] Alias for tasks
|
|
71
88
|
setup state Create/select S3 state bucket
|
|
72
89
|
setup tables <env> Generate DynamoDB tables from schema
|
|
73
90
|
setup frontend <env> Generate S3 + CloudFront infrastructure
|
|
@@ -79,6 +96,10 @@ module Belt
|
|
|
79
96
|
output <env> terraform output for environment
|
|
80
97
|
--version Show Belt version
|
|
81
98
|
|
|
99
|
+
Rake Tasks:
|
|
100
|
+
Any rake task from your Gemfile dependencies can be run directly:
|
|
101
|
+
belt lambda:build_layer Run a rake task by name
|
|
102
|
+
|
|
82
103
|
Environment:
|
|
83
104
|
Set BELT_ENV to skip the <env> argument:
|
|
84
105
|
export BELT_ENV=wups
|
|
@@ -92,6 +113,8 @@ module Belt
|
|
|
92
113
|
belt setup frontend wups
|
|
93
114
|
belt deploy frontend wups
|
|
94
115
|
belt apply wups
|
|
116
|
+
belt tasks # list all rake tasks
|
|
117
|
+
belt lambda:build_layer # run a rake task directly
|
|
95
118
|
USAGE
|
|
96
119
|
end
|
|
97
120
|
end
|
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
|