regolith 0.1.14 → 0.1.18
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/regolith/cli.rb +718 -491
- data/lib/regolith/service_client.rb +26 -82
- data/lib/regolith/version.rb +1 -1
- data/lib/regolith.rb +8 -8
- metadata +1 -1
data/lib/regolith/cli.rb
CHANGED
@@ -1,13 +1,62 @@
|
|
1
1
|
require 'fileutils'
|
2
2
|
require 'yaml'
|
3
|
+
require 'psych'
|
3
4
|
require 'erb'
|
4
5
|
require 'timeout'
|
5
6
|
require 'rubygems'
|
7
|
+
require 'net/http'
|
8
|
+
require 'uri'
|
9
|
+
require 'json'
|
10
|
+
require 'ostruct'
|
11
|
+
require 'set'
|
6
12
|
require 'rbconfig'
|
7
13
|
require 'shellwords'
|
8
|
-
require 'psych'
|
9
14
|
|
10
15
|
module Regolith
|
16
|
+
# NOTE: VERSION is defined in lib/regolith/version.rb.
|
17
|
+
# Do not define Regolith::VERSION here to avoid constant redefinition warnings.
|
18
|
+
|
19
|
+
class << self
|
20
|
+
# Lightweight configuration with sane defaults
|
21
|
+
def configuration
|
22
|
+
@configuration ||= OpenStruct.new(
|
23
|
+
timeout: 10,
|
24
|
+
default_port: 3001
|
25
|
+
)
|
26
|
+
end
|
27
|
+
|
28
|
+
def service_registry
|
29
|
+
@service_registry ||= begin
|
30
|
+
path = find_regolith_config
|
31
|
+
if path && File.exist?(path)
|
32
|
+
config = YAML.load_file(path) || {}
|
33
|
+
config['services'] || {}
|
34
|
+
else
|
35
|
+
{}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def reload_service_registry!
|
41
|
+
@service_registry = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def find_regolith_config
|
47
|
+
current_dir = Dir.pwd
|
48
|
+
loop do
|
49
|
+
config_path = File.join(current_dir, 'config', 'regolith.yml')
|
50
|
+
return config_path if File.exist?(config_path)
|
51
|
+
|
52
|
+
parent = File.dirname(current_dir)
|
53
|
+
break if parent == current_dir
|
54
|
+
current_dir = parent
|
55
|
+
end
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
11
60
|
class CLI
|
12
61
|
def initialize(args)
|
13
62
|
@args = args
|
@@ -23,9 +72,9 @@ module Regolith
|
|
23
72
|
when 'generate'
|
24
73
|
generate_resource(@subcommand, @name)
|
25
74
|
when 'server', 'up'
|
26
|
-
start_server
|
75
|
+
start_server(parse_flags(@args[1..-1]))
|
27
76
|
when 'down'
|
28
|
-
|
77
|
+
stop_services
|
29
78
|
when 'restart'
|
30
79
|
restart_service(@subcommand)
|
31
80
|
when 'stop'
|
@@ -33,36 +82,36 @@ module Regolith
|
|
33
82
|
when 'ps', 'status'
|
34
83
|
show_status
|
35
84
|
when 'logs'
|
36
|
-
show_logs(@subcommand, @args[2]
|
85
|
+
show_logs(@subcommand, parse_flags(@args[2..-1]))
|
37
86
|
when 'exec'
|
38
|
-
|
87
|
+
exec_command(@subcommand, @args[2..-1])
|
39
88
|
when 'shell'
|
40
89
|
shell_service(@subcommand)
|
41
|
-
when 'rails'
|
42
|
-
rails_passthrough(@subcommand, @args[2..-1])
|
43
90
|
when 'console'
|
44
91
|
open_console(@subcommand)
|
92
|
+
when 'rails'
|
93
|
+
rails_passthrough(@subcommand, @args[2..-1])
|
45
94
|
when 'routes'
|
46
95
|
show_routes(@subcommand)
|
47
96
|
when 'open'
|
48
97
|
open_service(@subcommand)
|
49
|
-
when 'db
|
50
|
-
db_command(@
|
98
|
+
when 'db'
|
99
|
+
db_command(@subcommand, @name, parse_flags(@args[3..-1]))
|
51
100
|
when 'test'
|
52
|
-
run_tests(@subcommand)
|
101
|
+
run_tests(@subcommand, parse_flags(@args[2..-1]))
|
53
102
|
when 'health'
|
54
|
-
|
103
|
+
health_check
|
55
104
|
when 'config'
|
56
|
-
show_config(@
|
57
|
-
when 'inspect'
|
58
|
-
inspect_config(@subcommand == '--json')
|
59
|
-
when 'doctor'
|
60
|
-
run_diagnostics
|
105
|
+
show_config(parse_flags(@args[1..-1]))
|
61
106
|
when 'prune'
|
62
|
-
|
107
|
+
prune_system
|
63
108
|
when 'rebuild'
|
64
109
|
rebuild_service(@subcommand)
|
65
|
-
when '
|
110
|
+
when 'doctor'
|
111
|
+
run_doctor
|
112
|
+
when 'inspect'
|
113
|
+
inspect_services(parse_flags(@args[1..-1]))
|
114
|
+
when 'version', '--version', '-v'
|
66
115
|
puts "Regolith #{Regolith::VERSION}"
|
67
116
|
else
|
68
117
|
show_help
|
@@ -71,6 +120,23 @@ module Regolith
|
|
71
120
|
|
72
121
|
private
|
73
122
|
|
123
|
+
def parse_flags(args)
|
124
|
+
flags = {}
|
125
|
+
return flags unless args
|
126
|
+
|
127
|
+
args.each do |arg|
|
128
|
+
if arg.start_with?('--')
|
129
|
+
key, value = arg[2..-1].split('=', 2)
|
130
|
+
flags[key.to_sym] = value || true
|
131
|
+
elsif arg == '-f'
|
132
|
+
flags[:follow] = true
|
133
|
+
elsif arg == '--all'
|
134
|
+
flags[:all] = true
|
135
|
+
end
|
136
|
+
end
|
137
|
+
flags
|
138
|
+
end
|
139
|
+
|
74
140
|
def create_new_app(app_name)
|
75
141
|
unless app_name
|
76
142
|
puts "❌ Error: App name required"
|
@@ -108,6 +174,8 @@ module Regolith
|
|
108
174
|
File.write('docker-compose.yml', generate_docker_compose(app_name))
|
109
175
|
File.write('Makefile', generate_makefile)
|
110
176
|
File.write('.bin/regolith', generate_regolith_shim)
|
177
|
+
File.write('.gitignore', generate_gitignore)
|
178
|
+
File.write('README.md', generate_readme(app_name))
|
111
179
|
FileUtils.chmod(0755, '.bin/regolith')
|
112
180
|
end
|
113
181
|
|
@@ -118,16 +186,17 @@ module Regolith
|
|
118
186
|
exit 1
|
119
187
|
end
|
120
188
|
|
121
|
-
# Validate service name
|
122
|
-
unless resource_name =~ /\A[a-z][a-z0-9_]*\z/
|
123
|
-
puts "❌ Invalid service name. Use lowercase letters, digits, and underscores."
|
124
|
-
exit 1
|
125
|
-
end
|
126
|
-
|
127
189
|
generate_service(resource_name)
|
128
190
|
end
|
129
191
|
|
130
192
|
def generate_service(service_name)
|
193
|
+
# Validate service name
|
194
|
+
unless service_name =~ /\A[a-z][a-z0-9_]*\z/
|
195
|
+
puts "❌ Invalid service name. Use lowercase, digits, and underscores only."
|
196
|
+
puts "Examples: users, user_profiles, api_gateway"
|
197
|
+
exit 1
|
198
|
+
end
|
199
|
+
|
131
200
|
puts "🔧 Creating service '#{service_name}'..."
|
132
201
|
config = load_regolith_config
|
133
202
|
port = next_available_port
|
@@ -161,262 +230,213 @@ module Regolith
|
|
161
230
|
exit 1
|
162
231
|
end
|
163
232
|
|
164
|
-
|
165
|
-
puts "🔧 Creating custom Gemfile..."
|
166
|
-
|
167
|
-
# Remove Rails-generated Gemfile and lock file to avoid conflicts
|
168
|
-
FileUtils.rm_f("#{service_dir}/Gemfile")
|
169
|
-
FileUtils.rm_f("#{service_dir}/Gemfile.lock")
|
170
|
-
|
171
|
-
generate_gemfile(service_dir)
|
172
|
-
|
173
|
-
puts " → Running bundle install..."
|
174
|
-
Dir.chdir(service_dir) do
|
175
|
-
unless system("bundle install")
|
176
|
-
puts "❌ bundle install failed"
|
177
|
-
puts "→ You may be missing system libraries like libyaml-dev libsqlite3-dev build-essential pkg-config"
|
178
|
-
puts "→ Try: sudo apt install -y libyaml-dev libsqlite3-dev build-essential pkg-config"
|
179
|
-
exit 1
|
180
|
-
end
|
181
|
-
end
|
182
|
-
|
183
|
-
patch_rails_app(service_dir, service_name, port)
|
233
|
+
customize_service(service_dir, service_name, port)
|
184
234
|
|
185
235
|
config['services'][service_name] = {
|
186
236
|
'port' => port,
|
187
237
|
'root' => "./#{service_dir}"
|
188
238
|
}
|
239
|
+
|
189
240
|
save_regolith_config(config)
|
190
241
|
update_docker_compose(config)
|
191
242
|
|
192
243
|
puts "✅ Created service '#{service_name}'"
|
193
244
|
puts "🚀 Service will run on port #{port}"
|
194
|
-
puts "→ Next: regolith server"
|
245
|
+
puts "→ Next: regolith generate service <another_service> or regolith server"
|
195
246
|
end
|
196
247
|
|
197
|
-
def next_available_port(start = 3001)
|
198
|
-
|
199
|
-
used_ports = config['services']&.values&.map { |s| s['port'] }.to_set || Set.new
|
248
|
+
def next_available_port(start = (Regolith.configuration.respond_to?(:default_port) ? Regolith.configuration.default_port : 3001))
|
249
|
+
used = load_regolith_config['services'].values.map { |s| s['port'] }.to_set
|
200
250
|
port = start
|
201
|
-
port += 1 while
|
251
|
+
port += 1 while used.include?(port)
|
202
252
|
port
|
203
253
|
end
|
204
254
|
|
205
|
-
def
|
206
|
-
|
207
|
-
|
208
|
-
|
255
|
+
def customize_service(service_dir, service_name, port)
|
256
|
+
# Detect Ruby MAJOR.MINOR
|
257
|
+
major_minor = RUBY_VERSION.split(".")[0..1].join(".")
|
258
|
+
|
259
|
+
# Write Gemfile (no vendoring; pull from RubyGems)
|
260
|
+
custom_gemfile = generate_gemfile(major_minor)
|
261
|
+
File.write("#{service_dir}/Gemfile", custom_gemfile)
|
262
|
+
|
263
|
+
puts " → Running bundle install..."
|
264
|
+
Dir.chdir(service_dir) do
|
265
|
+
unless system("bundle install")
|
266
|
+
puts "❌ bundle install failed"
|
267
|
+
puts "→ You may be missing system libraries like libyaml-dev build-essential"
|
268
|
+
exit 1
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
patch_rails_app(service_dir, service_name, port)
|
273
|
+
end
|
274
|
+
|
275
|
+
def generate_gemfile(ruby_version)
|
276
|
+
<<~GEMFILE
|
209
277
|
source "https://rubygems.org"
|
210
|
-
|
278
|
+
|
211
279
|
ruby "~> #{ruby_version}.0"
|
212
|
-
|
280
|
+
|
213
281
|
gem "rails", "~> 7.2.2.1"
|
214
282
|
gem "pg", "~> 1.5"
|
215
283
|
gem "puma", ">= 5.0"
|
216
284
|
gem "rack-cors"
|
217
|
-
|
285
|
+
gem "bootsnap", require: false
|
286
|
+
|
218
287
|
group :development, :test do
|
219
288
|
gem "debug", platforms: %i[mri mswin mswin64 mingw x64_mingw], require: "debug/prelude"
|
220
289
|
gem "brakeman", require: false
|
221
290
|
gem "rubocop-rails-omakase", require: false
|
222
291
|
end
|
223
|
-
|
224
|
-
gem "regolith", "~> 0.1.13"
|
225
|
-
GEMFILE
|
226
|
-
|
227
|
-
File.write("#{service_dir}/Gemfile", custom_gemfile)
|
228
|
-
end
|
229
|
-
|
230
|
-
|
231
|
-
def vendor_regolith_gem(service_dir)
|
232
|
-
vendor_dir = File.join(service_dir, "vendor")
|
233
|
-
FileUtils.mkdir_p(vendor_dir)
|
234
|
-
|
235
|
-
begin
|
236
|
-
# Try to find the regolith gem path
|
237
|
-
regolith_gem_path = if Gem.loaded_specs['regolith']
|
238
|
-
Gem.loaded_specs['regolith'].full_gem_path
|
239
|
-
else
|
240
|
-
# Fallback: search gem paths
|
241
|
-
Gem.path.each do |gem_path|
|
242
|
-
regolith_path = File.join(gem_path, 'gems', "regolith-#{Regolith::VERSION}")
|
243
|
-
return regolith_path if File.exist?(regolith_path)
|
244
|
-
end
|
245
|
-
nil
|
246
|
-
end
|
247
292
|
|
248
|
-
|
249
|
-
|
250
|
-
FileUtils.cp_r(regolith_gem_path, regolith_dest)
|
251
|
-
puts "📦 Vendored Regolith gem from #{regolith_gem_path}"
|
252
|
-
else
|
253
|
-
puts "⚠️ Could not find regolith gem path, using system gem instead"
|
254
|
-
# Update Gemfile to use system gem
|
255
|
-
gemfile_path = File.join(service_dir, "Gemfile")
|
256
|
-
gemfile_content = File.read(gemfile_path)
|
257
|
-
gemfile_content.gsub!('gem "regolith", path: "vendor/regolith"', 'gem "regolith", "~> 0.1.7"')
|
258
|
-
File.write(gemfile_path, gemfile_content)
|
259
|
-
end
|
260
|
-
rescue => e
|
261
|
-
puts "⚠️ Error vendoring regolith gem: #{e.message}"
|
262
|
-
puts " Using system gem instead"
|
263
|
-
# Update Gemfile to use system gem
|
264
|
-
gemfile_path = File.join(service_dir, "Gemfile")
|
265
|
-
gemfile_content = File.read(gemfile_path)
|
266
|
-
gemfile_content.gsub!('gem "regolith", path: "vendor/regolith"', 'gem "regolith", "~> 0.1.7"')
|
267
|
-
File.write(gemfile_path, gemfile_content)
|
268
|
-
end
|
293
|
+
gem "regolith"
|
294
|
+
GEMFILE
|
269
295
|
end
|
270
296
|
|
271
297
|
def patch_rails_app(service_dir, service_name, port)
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
# Add health route
|
298
|
+
create_initializers(service_dir, service_name)
|
299
|
+
create_health_controller(service_dir)
|
276
300
|
add_health_route(service_dir)
|
277
|
-
|
278
|
-
|
301
|
+
File.write("#{service_dir}/Dockerfile", generate_dockerfile)
|
302
|
+
patch_application_rb(service_dir, service_name, port)
|
303
|
+
end
|
304
|
+
|
305
|
+
def create_initializers(service_dir, service_name)
|
279
306
|
initializer_dir = "#{service_dir}/config/initializers"
|
280
307
|
FileUtils.mkdir_p(initializer_dir)
|
308
|
+
|
281
309
|
File.write("#{initializer_dir}/regolith.rb", generate_regolith_initializer(service_name))
|
282
|
-
|
283
|
-
|
284
|
-
|
310
|
+
File.write("#{initializer_dir}/cors.rb", generate_cors_initializer)
|
311
|
+
end
|
312
|
+
|
313
|
+
def create_health_controller(service_dir)
|
314
|
+
controller_dir = "#{service_dir}/app/controllers/regolith"
|
315
|
+
FileUtils.mkdir_p(controller_dir)
|
316
|
+
|
317
|
+
File.write("#{controller_dir}/health_controller.rb", generate_health_controller)
|
318
|
+
end
|
319
|
+
|
320
|
+
def add_health_route(service_dir)
|
321
|
+
routes_path = File.join(service_dir, "config/routes.rb")
|
322
|
+
content = File.read(routes_path)
|
323
|
+
|
324
|
+
# Try to inject before the final end, or append if no clear structure
|
325
|
+
if content.sub!(/end\s*\z/, " get '/health', to: 'regolith/health#show'\nend\n")
|
326
|
+
File.write(routes_path, content)
|
327
|
+
else
|
328
|
+
# Fallback: append inside the draw block
|
329
|
+
File.open(routes_path, "a") { |f| f.puts "get '/health', to: 'regolith/health#show'" }
|
330
|
+
end
|
331
|
+
end
|
285
332
|
|
286
|
-
|
333
|
+
def patch_application_rb(service_dir, service_name, port)
|
287
334
|
app_rb_path = "#{service_dir}/config/application.rb"
|
288
335
|
app_rb_content = File.read(app_rb_path)
|
289
336
|
|
290
337
|
cors_config = <<~RUBY
|
291
338
|
|
292
339
|
# Regolith configuration
|
293
|
-
config.middleware.insert_before 0, Rack::Cors do
|
294
|
-
allow do
|
295
|
-
origins '*'
|
296
|
-
resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head]
|
297
|
-
end
|
298
|
-
end
|
299
|
-
|
300
340
|
config.regolith_service_name = '#{service_name}'
|
301
341
|
config.regolith_service_port = #{port}
|
302
342
|
RUBY
|
303
343
|
|
304
|
-
app_rb_content.gsub!(/
|
305
|
-
end/m) do |match|
|
306
|
-
match.gsub(/(\n end)$/, "#{cors_config}\1")
|
307
|
-
end
|
308
|
-
|
344
|
+
app_rb_content.gsub!(/(\n end\n\z)/, "#{cors_config}\1")
|
309
345
|
File.write(app_rb_path, app_rb_content)
|
310
346
|
end
|
311
347
|
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
health_controller = <<~RUBY
|
317
|
-
module Regolith
|
318
|
-
class HealthController < ActionController::API
|
319
|
-
def show
|
320
|
-
render json: {
|
321
|
-
ok: true,
|
322
|
-
service: Rails.application.config.regolith_service_name,
|
323
|
-
version: Regolith::VERSION,
|
324
|
-
time: Time.now.iso8601
|
325
|
-
}
|
326
|
-
end
|
327
|
-
end
|
328
|
-
end
|
329
|
-
RUBY
|
330
|
-
|
331
|
-
File.write(File.join(controller_dir, "health_controller.rb"), health_controller)
|
332
|
-
end
|
333
|
-
|
334
|
-
def add_health_route(service_dir)
|
335
|
-
routes_path = File.join(service_dir, "config", "routes.rb")
|
336
|
-
content = File.read(routes_path)
|
337
|
-
|
338
|
-
if content.sub!(/end\s*\z/, " get '/health', to: 'regolith/health#show'\nend\n")
|
339
|
-
File.write(routes_path, content)
|
340
|
-
else
|
341
|
-
# Fallback: append to file
|
342
|
-
File.open(routes_path, "a") { |f| f.puts "\nget '/health', to: 'regolith/health#show'\n" }
|
343
|
-
end
|
344
|
-
end
|
345
|
-
|
346
|
-
def start_server
|
347
|
-
unless find_regolith_config
|
348
|
+
# Service management commands
|
349
|
+
def start_server(_flags = {})
|
350
|
+
unless File.exist?('docker-compose.yml')
|
348
351
|
puts "❌ Error: Not in a Regolith app directory"
|
349
352
|
exit 1
|
350
353
|
end
|
351
354
|
|
352
355
|
puts "🚀 Starting Regolith services..."
|
353
|
-
config = load_regolith_config
|
354
|
-
|
355
|
-
config['services'].each do |name, service|
|
356
|
-
puts "🚀 #{name}_service will run at http://localhost:#{service['port']}"
|
357
|
-
end
|
358
356
|
|
359
|
-
|
360
|
-
|
357
|
+
config = load_regolith_config
|
358
|
+
show_service_info(config)
|
361
359
|
|
362
|
-
exec_compose(
|
360
|
+
exec_compose('up', '--build')
|
363
361
|
end
|
364
362
|
|
365
|
-
def
|
366
|
-
|
363
|
+
def stop_services
|
364
|
+
puts "🛑 Stopping all services..."
|
365
|
+
exec_compose('down', '-v')
|
367
366
|
end
|
368
367
|
|
369
|
-
def restart_service(service_name)
|
368
|
+
def restart_service(service_name = nil)
|
370
369
|
if service_name
|
371
|
-
|
370
|
+
ensure_service_exists!(service_name)
|
371
|
+
puts "🔄 Restarting service '#{service_name}'..."
|
372
|
+
exec_compose('restart', service_name)
|
372
373
|
else
|
373
|
-
|
374
|
+
puts "🔄 Restarting all services..."
|
375
|
+
exec_compose('restart')
|
374
376
|
end
|
375
377
|
end
|
376
378
|
|
377
|
-
def stop_service(service_name)
|
378
|
-
|
379
|
-
|
380
|
-
puts "
|
381
|
-
|
379
|
+
def stop_service(service_name = nil)
|
380
|
+
if service_name
|
381
|
+
ensure_service_exists!(service_name)
|
382
|
+
puts "⏹ Stopping service '#{service_name}'..."
|
383
|
+
exec_compose('stop', service_name)
|
384
|
+
else
|
385
|
+
puts "⏹ Stopping all services..."
|
386
|
+
exec_compose('stop')
|
382
387
|
end
|
383
|
-
exec_compose("stop", service_name)
|
384
388
|
end
|
385
389
|
|
386
390
|
def show_status
|
387
|
-
|
388
|
-
|
389
|
-
|
391
|
+
puts "📊 Service Status:"
|
392
|
+
puts
|
393
|
+
|
394
|
+
# Try different format options for maximum compatibility
|
390
395
|
success = system_compose('ps', '--format', 'table') ||
|
391
396
|
system_compose('ps', '--format', 'json') ||
|
392
397
|
system_compose('ps')
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
398
|
+
|
399
|
+
# Show summary counts and health check for exit code
|
400
|
+
if success
|
401
|
+
config = load_regolith_config
|
402
|
+
service_count = config['services'].size
|
397
403
|
ports = config['services'].values.map { |s| s['port'] }.sort
|
398
|
-
|
404
|
+
|
405
|
+
puts
|
406
|
+
puts "📋 Summary: #{service_count} services configured"
|
407
|
+
puts "🚪 Ports: #{ports.join(', ')}" if ports.any?
|
408
|
+
|
409
|
+
# Check health for proper exit code
|
410
|
+
healthy_services = 0
|
411
|
+
config['services'].each do |name, service_config|
|
412
|
+
port = service_config['port']
|
413
|
+
status = check_service_health(port)
|
414
|
+
healthy_services += 1 if status[:healthy]
|
415
|
+
end
|
416
|
+
|
417
|
+
if healthy_services < service_count
|
418
|
+
puts "⚠️ #{service_count - healthy_services} services unhealthy"
|
419
|
+
exit 1
|
420
|
+
end
|
399
421
|
end
|
400
|
-
|
401
|
-
exit($?.exitstatus) unless success
|
402
422
|
end
|
403
423
|
|
404
|
-
def show_logs(service_name,
|
424
|
+
def show_logs(service_name = nil, flags = {})
|
405
425
|
args = ['logs']
|
426
|
+
args << '--follow' if flags[:follow] || flags[:f]
|
406
427
|
args << service_name if service_name
|
407
|
-
|
428
|
+
|
408
429
|
exec_compose(*args)
|
409
430
|
end
|
410
431
|
|
411
|
-
def
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
432
|
+
def exec_command(service_name, command_args)
|
433
|
+
ensure_service_exists!(service_name)
|
434
|
+
|
435
|
+
if command_args.empty?
|
436
|
+
command_args = ['bash']
|
416
437
|
end
|
417
|
-
|
418
|
-
|
419
|
-
exec_compose('exec', service_name, *command)
|
438
|
+
|
439
|
+
exec_compose('exec', service_name, *command_args)
|
420
440
|
end
|
421
441
|
|
422
442
|
def shell_service(service_name)
|
@@ -425,23 +445,22 @@ module Regolith
|
|
425
445
|
puts "Usage: regolith shell <service_name>"
|
426
446
|
exit 1
|
427
447
|
end
|
448
|
+
|
449
|
+
ensure_service_exists!(service_name)
|
450
|
+
puts "🐚 Opening shell for #{service_name}_service..."
|
428
451
|
exec_compose('exec', service_name, 'bash')
|
429
452
|
end
|
430
453
|
|
454
|
+
# Rails integration commands
|
431
455
|
def rails_passthrough(service_name, rails_args)
|
432
|
-
|
433
|
-
puts "❌ Error: Service name required"
|
434
|
-
puts "Usage: regolith rails <service_name> <rails_command>"
|
435
|
-
exit 1
|
436
|
-
end
|
456
|
+
ensure_service_exists!(service_name)
|
437
457
|
|
438
|
-
|
439
|
-
|
440
|
-
puts "
|
458
|
+
if rails_args.empty?
|
459
|
+
puts "❌ Error: Rails command required"
|
460
|
+
puts "Usage: regolith rails <service> <command>"
|
441
461
|
exit 1
|
442
462
|
end
|
443
463
|
|
444
|
-
rails_args = ['--help'] if rails_args.empty?
|
445
464
|
exec_compose('exec', service_name, 'bash', '-lc',
|
446
465
|
"bundle exec rails #{Shellwords.join(rails_args)}")
|
447
466
|
end
|
@@ -453,25 +472,11 @@ module Regolith
|
|
453
472
|
exit 1
|
454
473
|
end
|
455
474
|
|
456
|
-
|
457
|
-
unless config['services'][service_name]
|
458
|
-
puts "❌ Error: Service '#{service_name}' not found"
|
459
|
-
exit 1
|
460
|
-
end
|
461
|
-
|
475
|
+
ensure_service_exists!(service_name)
|
462
476
|
puts "🧪 Opening Rails console for #{service_name}_service..."
|
463
477
|
exec_compose('exec', service_name, 'rails', 'console')
|
464
478
|
end
|
465
479
|
|
466
|
-
def show_routes(service_name)
|
467
|
-
unless service_name
|
468
|
-
puts "❌ Error: Service name required"
|
469
|
-
puts "Usage: regolith routes <service_name>"
|
470
|
-
exit 1
|
471
|
-
end
|
472
|
-
rails_passthrough(service_name, ['routes'])
|
473
|
-
end
|
474
|
-
|
475
480
|
def open_service(service_name)
|
476
481
|
unless service_name
|
477
482
|
puts "❌ Error: Service name required"
|
@@ -479,232 +484,296 @@ module Regolith
|
|
479
484
|
exit 1
|
480
485
|
end
|
481
486
|
|
487
|
+
ensure_service_exists!(service_name)
|
482
488
|
config = load_regolith_config
|
483
|
-
|
484
|
-
|
485
|
-
puts "❌ Error: Service '#{service_name}' not found"
|
486
|
-
exit 1
|
487
|
-
end
|
489
|
+
port = config['services'][service_name]['port']
|
490
|
+
url = "http://localhost:#{port}"
|
488
491
|
|
489
|
-
url = "http://localhost:#{service['port']}"
|
490
492
|
puts "🌐 Opening #{url}..."
|
491
493
|
|
494
|
+
# Cross-platform open command
|
492
495
|
case RbConfig::CONFIG['host_os']
|
493
496
|
when /mswin|mingw|cygwin/
|
494
497
|
system(%{start "" "#{url}"})
|
495
498
|
when /darwin/
|
496
|
-
system("open
|
497
|
-
when /linux|bsd/
|
498
|
-
system("xdg-open", url)
|
499
|
+
system("open #{url}")
|
499
500
|
else
|
500
|
-
|
501
|
+
system("xdg-open #{url}") || puts("Visit: #{url}")
|
501
502
|
end
|
502
503
|
end
|
503
504
|
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
505
|
+
# Database commands
|
506
|
+
def db_command(action, target, flags = {})
|
507
|
+
case action
|
508
|
+
when 'create', 'migrate', 'seed', 'reset', 'setup', 'drop'
|
509
|
+
each_target_service(target, flags) do |service|
|
510
|
+
puts "🗄 Running db:#{action} for #{service}..."
|
511
|
+
success = system_compose('exec', service, 'bash', '-lc', "bundle exec rails db:#{action}")
|
512
|
+
|
513
|
+
unless success
|
514
|
+
puts "❌ db:#{action} failed for #{service} (exit code: #{$?.exitstatus})"
|
515
|
+
exit 1 unless flags[:continue_on_failure]
|
516
|
+
end
|
512
517
|
end
|
513
|
-
elsif service_or_flag
|
514
|
-
run_single_db_command(service_or_flag, db_action)
|
515
518
|
else
|
516
|
-
puts "❌
|
517
|
-
puts "
|
519
|
+
puts "❌ Unknown db action: #{action}"
|
520
|
+
puts "Available: create, migrate, seed, reset, setup, drop"
|
518
521
|
exit 1
|
519
522
|
end
|
520
523
|
end
|
521
524
|
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
puts "
|
526
|
-
|
527
|
-
end
|
525
|
+
# Testing commands
|
526
|
+
def run_tests(target = nil, flags = {})
|
527
|
+
each_target_service(target, flags) do |service|
|
528
|
+
puts "🧪 Running tests for #{service}..."
|
529
|
+
success = system_compose('exec', service, 'bash', '-lc', 'bundle exec rails test')
|
528
530
|
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
531
|
+
unless success
|
532
|
+
puts "❌ Tests failed for #{service} (exit code: #{$?.exitstatus})"
|
533
|
+
exit 1 unless flags[:continue_on_failure]
|
534
|
+
end
|
533
535
|
end
|
534
|
-
|
536
|
+
|
537
|
+
puts "✅ All tests passed!" if flags[:all]
|
535
538
|
end
|
536
539
|
|
537
|
-
def
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
config['services'].each do |service_name, _|
|
542
|
-
puts "🧪 Running tests for #{service_name}..."
|
543
|
-
unless run_single_test(service_name)
|
544
|
-
all_passed = false
|
545
|
-
end
|
546
|
-
end
|
547
|
-
exit(1) unless all_passed
|
548
|
-
elsif service_or_flag
|
549
|
-
exit(1) unless run_single_test(service_or_flag)
|
550
|
-
else
|
551
|
-
puts "❌ Error: Service name or --all flag required"
|
552
|
-
puts "Usage: regolith test <service_name|--all>"
|
540
|
+
def show_routes(service_name)
|
541
|
+
unless service_name
|
542
|
+
puts "❌ Error: Service name required"
|
543
|
+
puts "Usage: regolith routes <service_name>"
|
553
544
|
exit 1
|
554
545
|
end
|
546
|
+
|
547
|
+
ensure_service_exists!(service_name)
|
548
|
+
puts "🛤 Routes for #{service_name}:"
|
549
|
+
system_compose('exec', service_name, 'bash', '-lc', 'bundle exec rails routes')
|
555
550
|
end
|
556
551
|
|
557
|
-
|
552
|
+
# Health and monitoring
|
553
|
+
def health_check
|
554
|
+
puts "🔍 Health Check Results:"
|
555
|
+
puts
|
556
|
+
|
558
557
|
config = load_regolith_config
|
559
|
-
unless config['services'][service_name]
|
560
|
-
puts "❌ Error: Service '#{service_name}' not found"
|
561
|
-
return false
|
562
|
-
end
|
563
558
|
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
559
|
+
config['services'].each do |name, service_config|
|
560
|
+
port = service_config['port']
|
561
|
+
status = check_service_health(port)
|
562
|
+
|
563
|
+
status_icon = status[:healthy] ? '✅' : '❌'
|
564
|
+
puts "#{status_icon} #{name} (port #{port}) - #{status[:message]}"
|
565
|
+
|
566
|
+
# Show additional health data if available
|
567
|
+
if status[:data] && status[:data]['version']
|
568
|
+
puts " Version: #{status[:data]['version']}"
|
569
|
+
end
|
570
|
+
if status[:data] && status[:data]['time']
|
571
|
+
puts " Last seen: #{Time.at(status[:data]['time']).strftime('%H:%M:%S')}"
|
572
|
+
end
|
568
573
|
end
|
569
|
-
true
|
570
574
|
end
|
571
575
|
|
572
|
-
def
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
576
|
+
def check_service_health(port)
|
577
|
+
begin
|
578
|
+
uri = URI("http://localhost:#{port}/health")
|
579
|
+
response = Net::HTTP.get_response(uri)
|
580
|
+
|
581
|
+
if response.code.to_i == 200
|
582
|
+
# Try to parse structured health data
|
583
|
+
health_data = JSON.parse(response.body) rescue {}
|
584
|
+
{
|
585
|
+
healthy: true,
|
586
|
+
message: 'healthy',
|
587
|
+
data: health_data
|
588
|
+
}
|
583
589
|
else
|
584
|
-
|
585
|
-
all_healthy = false
|
590
|
+
{ healthy: false, message: "HTTP #{response.code}" }
|
586
591
|
end
|
592
|
+
rescue
|
593
|
+
{ healthy: false, message: 'unreachable' }
|
587
594
|
end
|
588
|
-
|
589
|
-
exit(1) unless all_healthy
|
590
|
-
end
|
591
|
-
|
592
|
-
def check_service_health(service_name, port)
|
593
|
-
require_relative 'service_client'
|
594
|
-
response = Regolith::ServiceClient.health(service_name)
|
595
|
-
Regolith::ServiceClient.parsed_body(response) if response.code.to_i == 200
|
596
|
-
rescue => e
|
597
|
-
nil
|
598
595
|
end
|
599
596
|
|
600
|
-
|
597
|
+
# Configuration
|
598
|
+
def show_config(flags = {})
|
601
599
|
config = load_regolith_config
|
602
|
-
|
600
|
+
|
601
|
+
if flags[:json]
|
603
602
|
puts JSON.pretty_generate(config)
|
604
603
|
else
|
604
|
+
puts "📋 Current Configuration:"
|
605
|
+
puts
|
605
606
|
puts YAML.dump(config)
|
606
607
|
end
|
607
608
|
end
|
608
609
|
|
609
|
-
|
610
|
+
# Maintenance commands
|
611
|
+
def prune_system
|
612
|
+
puts "🧹 Pruning Docker system..."
|
613
|
+
exec_compose('down', '-v')
|
614
|
+
system('docker', 'system', 'prune', '-f')
|
615
|
+
puts "✅ System pruned"
|
616
|
+
end
|
617
|
+
|
618
|
+
def rebuild_service(service_name = nil)
|
619
|
+
if service_name
|
620
|
+
ensure_service_exists!(service_name)
|
621
|
+
puts "🔨 Rebuilding service '#{service_name}'..."
|
622
|
+
exec_compose('build', '--no-cache', service_name)
|
623
|
+
else
|
624
|
+
puts "🔨 Rebuilding all services..."
|
625
|
+
exec_compose('build', '--no-cache')
|
626
|
+
end
|
627
|
+
end
|
628
|
+
|
629
|
+
# System diagnostics
|
630
|
+
def run_doctor
|
631
|
+
puts "🩺 Regolith System Doctor"
|
632
|
+
puts "=" * 40
|
633
|
+
|
634
|
+
checks = [
|
635
|
+
{ name: "Docker", check: -> { system('docker --version > /dev/null 2>&1') } },
|
636
|
+
{ name: "Docker Compose", check: -> { docker_compose_available? } },
|
637
|
+
{ name: "Ruby", check: -> { system('ruby --version > /dev/null 2>&1') } },
|
638
|
+
{ name: "Rails", check: -> { system('rails --version > /dev/null 2>&1') } },
|
639
|
+
{ name: "PostgreSQL Client", check: -> { system('psql --version > /dev/null 2>&1') } }
|
640
|
+
]
|
641
|
+
|
642
|
+
checks.each do |check|
|
643
|
+
status = check[:check].call ? "✅" : "❌"
|
644
|
+
puts "#{status} #{check[:name]}"
|
645
|
+
end
|
646
|
+
|
647
|
+
puts
|
648
|
+
check_regolith_config
|
649
|
+
end
|
650
|
+
|
651
|
+
def check_regolith_config
|
652
|
+
puts "📋 Checking Regolith configuration..."
|
653
|
+
|
654
|
+
if File.exist?('config/regolith.yml')
|
655
|
+
config = load_regolith_config
|
656
|
+
|
657
|
+
if config['services'].empty?
|
658
|
+
puts "⚠️ No services configured"
|
659
|
+
else
|
660
|
+
puts "✅ Configuration valid (#{config['services'].size} services)"
|
661
|
+
end
|
662
|
+
else
|
663
|
+
puts "❌ No regolith.yml found - not in a Regolith project?"
|
664
|
+
end
|
665
|
+
end
|
666
|
+
|
667
|
+
def inspect_services(flags = {})
|
668
|
+
puts "🔍 Regolith Services Inspection"
|
669
|
+
puts "=" * 40
|
670
|
+
|
610
671
|
config = load_regolith_config
|
611
|
-
|
612
|
-
if
|
613
|
-
|
614
|
-
|
615
|
-
|
672
|
+
|
673
|
+
if config['services'].empty?
|
674
|
+
puts "No services configured yet."
|
675
|
+
return
|
676
|
+
end
|
677
|
+
|
678
|
+
if flags[:json]
|
679
|
+
# JSON output for automation
|
680
|
+
inspection_data = {
|
681
|
+
services: config['services'].map do |name, service_config|
|
682
|
+
{
|
683
|
+
name: name,
|
684
|
+
port: service_config['port'],
|
685
|
+
endpoint: "http://localhost:#{service_config['port']}",
|
686
|
+
root: service_config['root']
|
687
|
+
}
|
688
|
+
end,
|
689
|
+
config: config
|
616
690
|
}
|
617
|
-
|
618
|
-
inspection[:endpoints][name] = "http://localhost:#{service['port']}"
|
619
|
-
end
|
620
|
-
puts JSON.pretty_generate(inspection)
|
691
|
+
puts JSON.pretty_generate(inspection_data)
|
621
692
|
else
|
622
|
-
|
623
|
-
|
624
|
-
|
693
|
+
# Human-readable output
|
694
|
+
puts "\n📊 Service Endpoints:"
|
695
|
+
config['services'].each do |name, service_config|
|
696
|
+
port = service_config['port']
|
697
|
+
puts " #{name}: http://localhost:#{port}"
|
625
698
|
end
|
626
|
-
|
627
|
-
puts "📋 Full Configuration:"
|
699
|
+
|
700
|
+
puts "\n📋 Full Configuration:"
|
628
701
|
puts YAML.dump(config)
|
629
702
|
end
|
630
703
|
end
|
631
704
|
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
# Check Docker Compose
|
644
|
-
if system("docker compose version > /dev/null 2>&1") ||
|
645
|
-
system("docker-compose --version > /dev/null 2>&1")
|
646
|
-
puts "✅ Docker Compose is available"
|
705
|
+
# Helper methods
|
706
|
+
def docker_compose_available?
|
707
|
+
system('docker compose version > /dev/null 2>&1') ||
|
708
|
+
system('docker-compose version > /dev/null 2>&1')
|
709
|
+
end
|
710
|
+
|
711
|
+
def docker_compose_command
|
712
|
+
if system('docker compose version > /dev/null 2>&1')
|
713
|
+
%w[docker compose]
|
714
|
+
elsif system('docker-compose version > /dev/null 2>&1')
|
715
|
+
%w[docker-compose]
|
647
716
|
else
|
648
|
-
puts "❌ Docker Compose
|
717
|
+
puts "❌ Docker Compose not found"
|
718
|
+
exit 1
|
649
719
|
end
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
720
|
+
end
|
721
|
+
|
722
|
+
def exec_compose(*args)
|
723
|
+
cmd = docker_compose_command + args
|
724
|
+
exec(*cmd)
|
725
|
+
end
|
726
|
+
|
727
|
+
def system_compose(*args)
|
728
|
+
cmd = docker_compose_command + args
|
729
|
+
system(*cmd)
|
730
|
+
end
|
731
|
+
|
732
|
+
def each_target_service(target, flags = {})
|
733
|
+
services = if target == '--all' || target.nil? || flags[:all]
|
734
|
+
load_regolith_config['services'].keys
|
735
|
+
else
|
736
|
+
[target]
|
737
|
+
end
|
738
|
+
|
739
|
+
services.each do |service|
|
740
|
+
ensure_service_exists!(service)
|
741
|
+
yield(service)
|
668
742
|
end
|
669
743
|
end
|
670
744
|
|
671
|
-
def
|
672
|
-
|
673
|
-
|
745
|
+
def ensure_service_exists!(service_name)
|
746
|
+
config = load_regolith_config
|
747
|
+
unless config['services'].key?(service_name)
|
748
|
+
puts "❌ Service '#{service_name}' not found"
|
749
|
+
puts "Available services: #{config['services'].keys.join(', ')}"
|
750
|
+
exit 1
|
751
|
+
end
|
674
752
|
end
|
675
753
|
|
676
|
-
def
|
677
|
-
|
678
|
-
puts "
|
679
|
-
exec_compose("build", "--no-cache", service_name)
|
680
|
-
else
|
681
|
-
puts "🔨 Rebuilding all services..."
|
682
|
-
exec_compose("build", "--no-cache")
|
754
|
+
def show_service_info(config)
|
755
|
+
config['services'].each do |name, service|
|
756
|
+
puts "🚀 #{name}_service: http://localhost:#{service['port']}"
|
683
757
|
end
|
758
|
+
|
759
|
+
puts "🧭 Service registry: config/regolith.yml"
|
760
|
+
puts
|
684
761
|
end
|
685
762
|
|
686
763
|
def load_regolith_config
|
687
|
-
config_path = find_regolith_config
|
688
|
-
return { 'services' => {} } unless config_path
|
689
|
-
|
764
|
+
config_path = Regolith.send(:find_regolith_config)
|
765
|
+
return { 'services' => {} } unless config_path && File.exist?(config_path)
|
766
|
+
|
767
|
+
# Use safe YAML loading
|
690
768
|
config = Psych.safe_load(File.read(config_path), permitted_classes: [], aliases: false) || {}
|
691
769
|
config['services'] ||= {}
|
692
770
|
config
|
693
771
|
end
|
694
772
|
|
695
|
-
def find_regolith_config
|
696
|
-
current_dir = Dir.pwd
|
697
|
-
while current_dir != '/'
|
698
|
-
config_path = File.join(current_dir, 'config', 'regolith.yml')
|
699
|
-
return config_path if File.exist?(config_path)
|
700
|
-
current_dir = File.dirname(current_dir)
|
701
|
-
end
|
702
|
-
nil
|
703
|
-
end
|
704
|
-
|
705
773
|
def save_regolith_config(config)
|
706
774
|
FileUtils.mkdir_p('config')
|
707
775
|
File.write('config/regolith.yml', YAML.dump(config))
|
776
|
+
Regolith.reload_service_registry!
|
708
777
|
end
|
709
778
|
|
710
779
|
def update_docker_compose(config)
|
@@ -712,82 +781,95 @@ module Regolith
|
|
712
781
|
File.write('docker-compose.yml', docker_compose)
|
713
782
|
end
|
714
783
|
|
784
|
+
# File generators
|
715
785
|
def generate_docker_compose(app_name, services = {})
|
716
|
-
|
717
|
-
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
747
|
-
|
748
|
-
|
749
|
-
|
750
|
-
|
751
|
-
|
752
|
-
|
753
|
-
|
754
|
-
|
755
|
-
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
|
766
|
-
|
767
|
-
|
768
|
-
|
769
|
-
|
770
|
-
|
771
|
-
|
772
|
-
|
786
|
+
compose_services = {
|
787
|
+
'db' => {
|
788
|
+
'image' => 'postgres:14',
|
789
|
+
'environment' => {
|
790
|
+
'POSTGRES_DB' => "#{app_name}_development",
|
791
|
+
'POSTGRES_USER' => 'postgres',
|
792
|
+
'POSTGRES_PASSWORD' => 'password'
|
793
|
+
},
|
794
|
+
'ports' => ['5432:5432'],
|
795
|
+
'volumes' => ['postgres_data:/var/lib/postgresql/data'],
|
796
|
+
'networks' => ['regolith'],
|
797
|
+
'healthcheck' => {
|
798
|
+
'test' => ['CMD-SHELL', 'pg_isready -U postgres'],
|
799
|
+
'interval' => '10s',
|
800
|
+
'timeout' => '5s',
|
801
|
+
'retries' => 5
|
802
|
+
}
|
803
|
+
}
|
804
|
+
}
|
805
|
+
|
806
|
+
services.each do |name, service|
|
807
|
+
compose_services[name] = {
|
808
|
+
'build' => {
|
809
|
+
'context' => service['root'],
|
810
|
+
'args' => {
|
811
|
+
'BUILD_ENV' => 'development'
|
812
|
+
}
|
813
|
+
},
|
814
|
+
'ports' => ["#{service['port']}:3000"],
|
815
|
+
'networks' => ['regolith'],
|
816
|
+
'depends_on' => {
|
817
|
+
'db' => {
|
818
|
+
'condition' => 'service_healthy'
|
819
|
+
}
|
820
|
+
},
|
821
|
+
'environment' => {
|
822
|
+
'DATABASE_URL' => "postgres://postgres:password@db:5432/#{app_name}_development",
|
823
|
+
'REGOLITH_SERVICE_NAME' => name,
|
824
|
+
'REGOLITH_SERVICE_PORT' => service['port']
|
825
|
+
},
|
826
|
+
'volumes' => ["#{service['root']}:/app"],
|
827
|
+
'command' => 'bash -c "rm -f tmp/pids/server.pid && bundle install && rails db:prepare && rails server -b 0.0.0.0"',
|
828
|
+
'healthcheck' => {
|
829
|
+
'test' => ['CMD-SHELL', 'curl -f http://localhost:3000/health || exit 1'],
|
830
|
+
'interval' => '30s',
|
831
|
+
'timeout' => '10s',
|
832
|
+
'retries' => 3,
|
833
|
+
'start_period' => '40s'
|
834
|
+
}
|
835
|
+
}
|
836
|
+
end
|
837
|
+
|
838
|
+
{
|
839
|
+
'version' => '3.8',
|
840
|
+
'networks' => {
|
841
|
+
'regolith' => {}
|
842
|
+
},
|
843
|
+
'services' => compose_services,
|
844
|
+
'volumes' => {
|
845
|
+
'postgres_data' => nil
|
846
|
+
}
|
847
|
+
}.to_yaml
|
773
848
|
end
|
774
849
|
|
775
850
|
def generate_dockerfile
|
776
851
|
<<~DOCKERFILE
|
777
|
-
FROM ruby:3.1
|
852
|
+
FROM ruby:3.1-slim
|
778
853
|
|
779
|
-
|
854
|
+
# Install system dependencies
|
855
|
+
RUN apt-get update -qq && \\
|
856
|
+
apt-get install -y nodejs postgresql-client libyaml-dev build-essential pkg-config curl && \\
|
857
|
+
apt-get clean && \\
|
858
|
+
rm -rf /var/lib/apt/lists/*
|
780
859
|
|
781
|
-
|
860
|
+
WORKDIR /app
|
782
861
|
|
862
|
+
# Copy Gemfile and install gems
|
783
863
|
COPY Gemfile Gemfile.lock* ./
|
784
864
|
|
865
|
+
# Conditional bundler config for dev vs prod
|
785
866
|
ARG BUILD_ENV=development
|
786
867
|
RUN if [ "$BUILD_ENV" = "production" ]; then \\
|
787
868
|
bundle config set --local deployment 'true' && \\
|
788
869
|
bundle config set --local without 'development test'; \\
|
789
870
|
fi && bundle install
|
790
871
|
|
872
|
+
# Copy application code
|
791
873
|
COPY . .
|
792
874
|
|
793
875
|
EXPOSE 3000
|
@@ -809,6 +891,7 @@ module Regolith
|
|
809
891
|
)
|
810
892
|
end
|
811
893
|
|
894
|
+
# Load service registry if available
|
812
895
|
if File.exist?(Rails.application.config.regolith.service_registry)
|
813
896
|
REGOLITH_SERVICES = YAML.load_file(Rails.application.config.regolith.service_registry)['services'] || {}
|
814
897
|
else
|
@@ -817,22 +900,184 @@ module Regolith
|
|
817
900
|
RUBY
|
818
901
|
end
|
819
902
|
|
903
|
+
def generate_cors_initializer
|
904
|
+
<<~RUBY
|
905
|
+
# CORS configuration for microservices
|
906
|
+
Rails.application.config.middleware.insert_before 0, Rack::Cors do
|
907
|
+
allow do
|
908
|
+
origins '*' # Configure appropriately for production
|
909
|
+
resource '*',
|
910
|
+
headers: :any,
|
911
|
+
methods: %i[get post put patch delete options head],
|
912
|
+
expose: %w[Authorization Content-Type],
|
913
|
+
max_age: 600
|
914
|
+
end
|
915
|
+
end
|
916
|
+
RUBY
|
917
|
+
end
|
918
|
+
|
919
|
+
def generate_health_controller
|
920
|
+
<<~RUBY
|
921
|
+
module Regolith
|
922
|
+
class HealthController < ActionController::API
|
923
|
+
def show
|
924
|
+
render json: {
|
925
|
+
ok: true,
|
926
|
+
service: Rails.application.config.regolith_service_name,
|
927
|
+
time: Time.now.to_i,
|
928
|
+
version: Rails.application.config.regolith.version
|
929
|
+
}
|
930
|
+
end
|
931
|
+
end
|
932
|
+
end
|
933
|
+
RUBY
|
934
|
+
end
|
935
|
+
|
936
|
+
def generate_gitignore
|
937
|
+
<<~GITIGNORE
|
938
|
+
# Regolith
|
939
|
+
/services/*/log/*
|
940
|
+
/services/*/tmp/*
|
941
|
+
/services/*/.env*
|
942
|
+
.DS_Store
|
943
|
+
|
944
|
+
# Docker
|
945
|
+
docker-compose.override.yml
|
946
|
+
|
947
|
+
# Logs
|
948
|
+
*.log
|
949
|
+
|
950
|
+
# Runtime data
|
951
|
+
pids
|
952
|
+
*.pid
|
953
|
+
*.seed
|
954
|
+
|
955
|
+
# Environment variables
|
956
|
+
.env*
|
957
|
+
!.env.example
|
958
|
+
GITIGNORE
|
959
|
+
end
|
960
|
+
|
961
|
+
def generate_readme(app_name)
|
962
|
+
<<~MARKDOWN
|
963
|
+
# #{app_name.capitalize}
|
964
|
+
|
965
|
+
A Regolith microservices application built with Rails and Docker.
|
966
|
+
|
967
|
+
## Getting Started
|
968
|
+
|
969
|
+
```bash
|
970
|
+
# Start all services
|
971
|
+
regolith server
|
972
|
+
|
973
|
+
# Generate a new service
|
974
|
+
regolith generate service users
|
975
|
+
|
976
|
+
# Open service in browser
|
977
|
+
regolith open users
|
978
|
+
|
979
|
+
# View service logs
|
980
|
+
regolith logs users -f
|
981
|
+
|
982
|
+
# Run database migrations
|
983
|
+
regolith db:migrate --all
|
984
|
+
|
985
|
+
# Health check
|
986
|
+
regolith health
|
987
|
+
```
|
988
|
+
|
989
|
+
## Services
|
990
|
+
|
991
|
+
#{services_documentation}
|
992
|
+
|
993
|
+
## Development
|
994
|
+
|
995
|
+
```bash
|
996
|
+
# Open Rails console for a service
|
997
|
+
regolith console users
|
998
|
+
|
999
|
+
# Run Rails commands
|
1000
|
+
regolith rails users db:migrate
|
1001
|
+
regolith rails users routes
|
1002
|
+
|
1003
|
+
# Run tests
|
1004
|
+
regolith test --all
|
1005
|
+
|
1006
|
+
# Execute commands in service
|
1007
|
+
regolith exec users bash
|
1008
|
+
```
|
1009
|
+
|
1010
|
+
## Architecture
|
1011
|
+
|
1012
|
+
- **Rails 7** API-only applications
|
1013
|
+
- **PostgreSQL** for persistence
|
1014
|
+
- **Docker Compose** for orchestration
|
1015
|
+
- **Service registry** for inter-service communication
|
1016
|
+
|
1017
|
+
Built with [Regolith](https://regolith.bio) - Rails for distributed systems.
|
1018
|
+
MARKDOWN
|
1019
|
+
end
|
1020
|
+
|
1021
|
+
def services_documentation
|
1022
|
+
config = load_regolith_config
|
1023
|
+
return "No services yet. Run `regolith generate service <name>` to create one." if config['services'].empty?
|
1024
|
+
|
1025
|
+
config['services'].map do |name, service|
|
1026
|
+
"- **#{name}** - http://localhost:#{service['port']}"
|
1027
|
+
end.join("\n")
|
1028
|
+
end
|
1029
|
+
|
820
1030
|
def generate_makefile
|
821
1031
|
<<~MAKEFILE
|
822
|
-
.PHONY: server console
|
1032
|
+
.PHONY: server up down restart logs console test health doctor
|
823
1033
|
|
824
|
-
|
1034
|
+
# Start services
|
1035
|
+
server up:
|
825
1036
|
\tregolith server
|
826
1037
|
|
1038
|
+
# Stop services
|
1039
|
+
down:
|
1040
|
+
\tregolith down
|
1041
|
+
|
1042
|
+
# Restart services
|
1043
|
+
restart:
|
1044
|
+
\tregolith restart
|
1045
|
+
|
1046
|
+
# View logs
|
1047
|
+
logs:
|
1048
|
+
\tregolith logs -f
|
1049
|
+
|
1050
|
+
# Open console (usage: make console SERVICE=users)
|
827
1051
|
console:
|
828
|
-
\
|
1052
|
+
\t@if [ -z "$(SERVICE)" ]; then echo "Usage: make console SERVICE=service_name"; exit 1; fi
|
1053
|
+
\tregolith console $(SERVICE)
|
1054
|
+
|
1055
|
+
# Run tests
|
1056
|
+
test:
|
1057
|
+
\tregolith test --all
|
1058
|
+
|
1059
|
+
# Health check
|
1060
|
+
health:
|
1061
|
+
\tregolith health
|
829
1062
|
|
830
|
-
|
831
|
-
|
1063
|
+
# System diagnostics
|
1064
|
+
doctor:
|
1065
|
+
\tregolith doctor
|
832
1066
|
|
1067
|
+
# Database operations
|
1068
|
+
db-migrate:
|
1069
|
+
\tregolith db:migrate --all
|
1070
|
+
|
1071
|
+
db-setup:
|
1072
|
+
\tregolith db:setup --all
|
1073
|
+
|
1074
|
+
# Cleanup
|
833
1075
|
clean:
|
834
|
-
\
|
835
|
-
|
1076
|
+
\tregolith prune
|
1077
|
+
|
1078
|
+
# Shortcuts
|
1079
|
+
dev: up
|
1080
|
+
stop: down
|
836
1081
|
MAKEFILE
|
837
1082
|
end
|
838
1083
|
|
@@ -843,82 +1088,64 @@ module Regolith
|
|
843
1088
|
RUBY
|
844
1089
|
end
|
845
1090
|
|
846
|
-
def exec_compose(*args)
|
847
|
-
if system("docker compose version > /dev/null 2>&1")
|
848
|
-
exec("docker", "compose", *args)
|
849
|
-
elsif system("docker-compose --version > /dev/null 2>&1")
|
850
|
-
exec("docker-compose", *args)
|
851
|
-
else
|
852
|
-
puts "❌ Error: Neither 'docker compose' nor 'docker-compose' is available"
|
853
|
-
exit 1
|
854
|
-
end
|
855
|
-
end
|
856
|
-
|
857
|
-
def system_compose(*args)
|
858
|
-
if system("docker compose version > /dev/null 2>&1")
|
859
|
-
system("docker", "compose", *args)
|
860
|
-
elsif system("docker-compose --version > /dev/null 2>&1")
|
861
|
-
system("docker-compose", *args)
|
862
|
-
else
|
863
|
-
puts "❌ Error: Neither 'docker compose' nor 'docker-compose' is available"
|
864
|
-
false
|
865
|
-
end
|
866
|
-
end
|
867
|
-
|
868
1091
|
def show_help
|
869
1092
|
puts <<~HELP
|
870
|
-
Regolith #{Regolith::VERSION} - Rails
|
1093
|
+
Regolith #{Regolith::VERSION} - Rails for Distributed Systems
|
871
1094
|
|
872
1095
|
USAGE:
|
873
1096
|
regolith <command> [options]
|
874
1097
|
|
875
|
-
PROJECT
|
1098
|
+
PROJECT COMMANDS:
|
876
1099
|
new <app_name> Create a new Regolith application
|
877
1100
|
generate service <name> Generate a new microservice
|
878
1101
|
|
879
1102
|
SERVICE MANAGEMENT:
|
880
1103
|
server, up Start all services with Docker Compose
|
881
|
-
down Stop all services
|
882
|
-
restart [service] Restart
|
883
|
-
stop
|
884
|
-
ps, status Show
|
885
|
-
logs [service] [-f]
|
886
|
-
exec <service>
|
887
|
-
shell <service> Open
|
1104
|
+
down Stop and remove all services
|
1105
|
+
restart [service] Restart one or all services
|
1106
|
+
stop [service] Stop one or all services
|
1107
|
+
ps, status Show running containers
|
1108
|
+
logs [service] [-f] View service logs
|
1109
|
+
exec <service> [cmd] Execute command in service container
|
1110
|
+
shell <service> Open shell in service container
|
1111
|
+
rebuild [service] Rebuild service images
|
888
1112
|
|
889
1113
|
RAILS INTEGRATION:
|
890
|
-
rails <service> <command> Run Rails command in service
|
891
1114
|
console <service> Open Rails console for service
|
1115
|
+
rails <service> <cmd> Run Rails command in service
|
892
1116
|
routes <service> Show routes for service
|
893
|
-
open <service> Open service
|
1117
|
+
open <service> Open service in browser
|
894
1118
|
|
895
1119
|
DATABASE OPERATIONS:
|
896
|
-
db:
|
897
|
-
db:
|
898
|
-
db:seed [service|--all]
|
899
|
-
db:reset [service|--all]
|
900
|
-
db:setup [service|--all]
|
901
|
-
db:drop [service|--all]
|
1120
|
+
db:migrate [service|--all] Run migrations
|
1121
|
+
db:create [service|--all] Create databases
|
1122
|
+
db:seed [service|--all] Seed databases
|
1123
|
+
db:reset [service|--all] Reset databases
|
1124
|
+
db:setup [service|--all] Setup databases
|
1125
|
+
db:drop [service|--all] Drop databases
|
902
1126
|
|
903
1127
|
TESTING & HEALTH:
|
904
1128
|
test [service|--all] Run tests
|
905
|
-
health
|
1129
|
+
health Health check all services
|
906
1130
|
|
907
1131
|
UTILITIES:
|
908
|
-
config [--json] Show configuration
|
909
|
-
inspect [--json] Show
|
1132
|
+
config [--json] Show current configuration
|
1133
|
+
inspect [--json] Show services and resolved endpoints
|
1134
|
+
prune Clean up Docker system
|
910
1135
|
doctor Run system diagnostics
|
911
|
-
|
912
|
-
rebuild [service] Rebuild service images
|
913
|
-
version Show version information
|
1136
|
+
version Show version
|
914
1137
|
|
915
1138
|
EXAMPLES:
|
916
1139
|
regolith new marketplace
|
917
1140
|
regolith generate service products
|
918
1141
|
regolith server
|
919
1142
|
regolith rails products db:migrate
|
1143
|
+
regolith routes products
|
920
1144
|
regolith open products
|
921
|
-
regolith
|
1145
|
+
regolith shell products
|
1146
|
+
regolith inspect --json
|
1147
|
+
regolith config --json | jq '.services'
|
1148
|
+
regolith test --all
|
922
1149
|
|
923
1150
|
Get started:
|
924
1151
|
regolith new myapp
|