regolith 0.1.15 → 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 +717 -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,263 +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
|
218
|
-
|
286
|
+
|
219
287
|
group :development, :test do
|
220
288
|
gem "debug", platforms: %i[mri mswin mswin64 mingw x64_mingw], require: "debug/prelude"
|
221
289
|
gem "brakeman", require: false
|
222
290
|
gem "rubocop-rails-omakase", require: false
|
223
291
|
end
|
224
|
-
|
225
|
-
gem "regolith", "~> 0.1.13"
|
226
|
-
GEMFILE
|
227
|
-
|
228
|
-
File.write("#{service_dir}/Gemfile", custom_gemfile)
|
229
|
-
end
|
230
|
-
|
231
|
-
|
232
|
-
def vendor_regolith_gem(service_dir)
|
233
|
-
vendor_dir = File.join(service_dir, "vendor")
|
234
|
-
FileUtils.mkdir_p(vendor_dir)
|
235
|
-
|
236
|
-
begin
|
237
|
-
# Try to find the regolith gem path
|
238
|
-
regolith_gem_path = if Gem.loaded_specs['regolith']
|
239
|
-
Gem.loaded_specs['regolith'].full_gem_path
|
240
|
-
else
|
241
|
-
# Fallback: search gem paths
|
242
|
-
Gem.path.each do |gem_path|
|
243
|
-
regolith_path = File.join(gem_path, 'gems', "regolith-#{Regolith::VERSION}")
|
244
|
-
return regolith_path if File.exist?(regolith_path)
|
245
|
-
end
|
246
|
-
nil
|
247
|
-
end
|
248
292
|
|
249
|
-
|
250
|
-
|
251
|
-
FileUtils.cp_r(regolith_gem_path, regolith_dest)
|
252
|
-
puts "📦 Vendored Regolith gem from #{regolith_gem_path}"
|
253
|
-
else
|
254
|
-
puts "⚠️ Could not find regolith gem path, using system gem instead"
|
255
|
-
# Update Gemfile to use system gem
|
256
|
-
gemfile_path = File.join(service_dir, "Gemfile")
|
257
|
-
gemfile_content = File.read(gemfile_path)
|
258
|
-
gemfile_content.gsub!('gem "regolith", path: "vendor/regolith"', 'gem "regolith", "~> 0.1.7"')
|
259
|
-
File.write(gemfile_path, gemfile_content)
|
260
|
-
end
|
261
|
-
rescue => e
|
262
|
-
puts "⚠️ Error vendoring regolith gem: #{e.message}"
|
263
|
-
puts " Using system gem instead"
|
264
|
-
# Update Gemfile to use system gem
|
265
|
-
gemfile_path = File.join(service_dir, "Gemfile")
|
266
|
-
gemfile_content = File.read(gemfile_path)
|
267
|
-
gemfile_content.gsub!('gem "regolith", path: "vendor/regolith"', 'gem "regolith", "~> 0.1.7"')
|
268
|
-
File.write(gemfile_path, gemfile_content)
|
269
|
-
end
|
293
|
+
gem "regolith"
|
294
|
+
GEMFILE
|
270
295
|
end
|
271
296
|
|
272
297
|
def patch_rails_app(service_dir, service_name, port)
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
# Add health route
|
298
|
+
create_initializers(service_dir, service_name)
|
299
|
+
create_health_controller(service_dir)
|
277
300
|
add_health_route(service_dir)
|
278
|
-
|
279
|
-
|
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)
|
280
306
|
initializer_dir = "#{service_dir}/config/initializers"
|
281
307
|
FileUtils.mkdir_p(initializer_dir)
|
308
|
+
|
282
309
|
File.write("#{initializer_dir}/regolith.rb", generate_regolith_initializer(service_name))
|
283
|
-
|
284
|
-
|
285
|
-
|
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
|
286
332
|
|
287
|
-
|
333
|
+
def patch_application_rb(service_dir, service_name, port)
|
288
334
|
app_rb_path = "#{service_dir}/config/application.rb"
|
289
335
|
app_rb_content = File.read(app_rb_path)
|
290
336
|
|
291
337
|
cors_config = <<~RUBY
|
292
338
|
|
293
339
|
# Regolith configuration
|
294
|
-
config.middleware.insert_before 0, Rack::Cors do
|
295
|
-
allow do
|
296
|
-
origins '*'
|
297
|
-
resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head]
|
298
|
-
end
|
299
|
-
end
|
300
|
-
|
301
340
|
config.regolith_service_name = '#{service_name}'
|
302
341
|
config.regolith_service_port = #{port}
|
303
342
|
RUBY
|
304
343
|
|
305
|
-
app_rb_content.gsub!(/
|
306
|
-
end/m) do |match|
|
307
|
-
match.gsub(/(\n end)$/, "#{cors_config}\1")
|
308
|
-
end
|
309
|
-
|
344
|
+
app_rb_content.gsub!(/(\n end\n\z)/, "#{cors_config}\1")
|
310
345
|
File.write(app_rb_path, app_rb_content)
|
311
346
|
end
|
312
347
|
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
health_controller = <<~RUBY
|
318
|
-
module Regolith
|
319
|
-
class HealthController < ActionController::API
|
320
|
-
def show
|
321
|
-
render json: {
|
322
|
-
ok: true,
|
323
|
-
service: Rails.application.config.regolith_service_name,
|
324
|
-
version: Regolith::VERSION,
|
325
|
-
time: Time.now.iso8601
|
326
|
-
}
|
327
|
-
end
|
328
|
-
end
|
329
|
-
end
|
330
|
-
RUBY
|
331
|
-
|
332
|
-
File.write(File.join(controller_dir, "health_controller.rb"), health_controller)
|
333
|
-
end
|
334
|
-
|
335
|
-
def add_health_route(service_dir)
|
336
|
-
routes_path = File.join(service_dir, "config", "routes.rb")
|
337
|
-
content = File.read(routes_path)
|
338
|
-
|
339
|
-
if content.sub!(/end\s*\z/, " get '/health', to: 'regolith/health#show'\nend\n")
|
340
|
-
File.write(routes_path, content)
|
341
|
-
else
|
342
|
-
# Fallback: append to file
|
343
|
-
File.open(routes_path, "a") { |f| f.puts "\nget '/health', to: 'regolith/health#show'\n" }
|
344
|
-
end
|
345
|
-
end
|
346
|
-
|
347
|
-
def start_server
|
348
|
-
unless find_regolith_config
|
348
|
+
# Service management commands
|
349
|
+
def start_server(_flags = {})
|
350
|
+
unless File.exist?('docker-compose.yml')
|
349
351
|
puts "❌ Error: Not in a Regolith app directory"
|
350
352
|
exit 1
|
351
353
|
end
|
352
354
|
|
353
355
|
puts "🚀 Starting Regolith services..."
|
354
|
-
config = load_regolith_config
|
355
|
-
|
356
|
-
config['services'].each do |name, service|
|
357
|
-
puts "🚀 #{name}_service will run at http://localhost:#{service['port']}"
|
358
|
-
end
|
359
356
|
|
360
|
-
|
361
|
-
|
357
|
+
config = load_regolith_config
|
358
|
+
show_service_info(config)
|
362
359
|
|
363
|
-
exec_compose(
|
360
|
+
exec_compose('up', '--build')
|
364
361
|
end
|
365
362
|
|
366
|
-
def
|
367
|
-
|
363
|
+
def stop_services
|
364
|
+
puts "🛑 Stopping all services..."
|
365
|
+
exec_compose('down', '-v')
|
368
366
|
end
|
369
367
|
|
370
|
-
def restart_service(service_name)
|
368
|
+
def restart_service(service_name = nil)
|
371
369
|
if service_name
|
372
|
-
|
370
|
+
ensure_service_exists!(service_name)
|
371
|
+
puts "🔄 Restarting service '#{service_name}'..."
|
372
|
+
exec_compose('restart', service_name)
|
373
373
|
else
|
374
|
-
|
374
|
+
puts "🔄 Restarting all services..."
|
375
|
+
exec_compose('restart')
|
375
376
|
end
|
376
377
|
end
|
377
378
|
|
378
|
-
def stop_service(service_name)
|
379
|
-
|
380
|
-
|
381
|
-
puts "
|
382
|
-
|
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')
|
383
387
|
end
|
384
|
-
exec_compose("stop", service_name)
|
385
388
|
end
|
386
389
|
|
387
390
|
def show_status
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
+
puts "📊 Service Status:"
|
392
|
+
puts
|
393
|
+
|
394
|
+
# Try different format options for maximum compatibility
|
391
395
|
success = system_compose('ps', '--format', 'table') ||
|
392
396
|
system_compose('ps', '--format', 'json') ||
|
393
397
|
system_compose('ps')
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
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
|
398
403
|
ports = config['services'].values.map { |s| s['port'] }.sort
|
399
|
-
|
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
|
400
421
|
end
|
401
|
-
|
402
|
-
exit($?.exitstatus) unless success
|
403
422
|
end
|
404
423
|
|
405
|
-
def show_logs(service_name,
|
424
|
+
def show_logs(service_name = nil, flags = {})
|
406
425
|
args = ['logs']
|
426
|
+
args << '--follow' if flags[:follow] || flags[:f]
|
407
427
|
args << service_name if service_name
|
408
|
-
|
428
|
+
|
409
429
|
exec_compose(*args)
|
410
430
|
end
|
411
431
|
|
412
|
-
def
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
432
|
+
def exec_command(service_name, command_args)
|
433
|
+
ensure_service_exists!(service_name)
|
434
|
+
|
435
|
+
if command_args.empty?
|
436
|
+
command_args = ['bash']
|
417
437
|
end
|
418
|
-
|
419
|
-
|
420
|
-
exec_compose('exec', service_name, *command)
|
438
|
+
|
439
|
+
exec_compose('exec', service_name, *command_args)
|
421
440
|
end
|
422
441
|
|
423
442
|
def shell_service(service_name)
|
@@ -426,23 +445,22 @@ module Regolith
|
|
426
445
|
puts "Usage: regolith shell <service_name>"
|
427
446
|
exit 1
|
428
447
|
end
|
448
|
+
|
449
|
+
ensure_service_exists!(service_name)
|
450
|
+
puts "🐚 Opening shell for #{service_name}_service..."
|
429
451
|
exec_compose('exec', service_name, 'bash')
|
430
452
|
end
|
431
453
|
|
454
|
+
# Rails integration commands
|
432
455
|
def rails_passthrough(service_name, rails_args)
|
433
|
-
|
434
|
-
puts "❌ Error: Service name required"
|
435
|
-
puts "Usage: regolith rails <service_name> <rails_command>"
|
436
|
-
exit 1
|
437
|
-
end
|
456
|
+
ensure_service_exists!(service_name)
|
438
457
|
|
439
|
-
|
440
|
-
|
441
|
-
puts "
|
458
|
+
if rails_args.empty?
|
459
|
+
puts "❌ Error: Rails command required"
|
460
|
+
puts "Usage: regolith rails <service> <command>"
|
442
461
|
exit 1
|
443
462
|
end
|
444
463
|
|
445
|
-
rails_args = ['--help'] if rails_args.empty?
|
446
464
|
exec_compose('exec', service_name, 'bash', '-lc',
|
447
465
|
"bundle exec rails #{Shellwords.join(rails_args)}")
|
448
466
|
end
|
@@ -454,25 +472,11 @@ module Regolith
|
|
454
472
|
exit 1
|
455
473
|
end
|
456
474
|
|
457
|
-
|
458
|
-
unless config['services'][service_name]
|
459
|
-
puts "❌ Error: Service '#{service_name}' not found"
|
460
|
-
exit 1
|
461
|
-
end
|
462
|
-
|
475
|
+
ensure_service_exists!(service_name)
|
463
476
|
puts "🧪 Opening Rails console for #{service_name}_service..."
|
464
477
|
exec_compose('exec', service_name, 'rails', 'console')
|
465
478
|
end
|
466
479
|
|
467
|
-
def show_routes(service_name)
|
468
|
-
unless service_name
|
469
|
-
puts "❌ Error: Service name required"
|
470
|
-
puts "Usage: regolith routes <service_name>"
|
471
|
-
exit 1
|
472
|
-
end
|
473
|
-
rails_passthrough(service_name, ['routes'])
|
474
|
-
end
|
475
|
-
|
476
480
|
def open_service(service_name)
|
477
481
|
unless service_name
|
478
482
|
puts "❌ Error: Service name required"
|
@@ -480,232 +484,296 @@ module Regolith
|
|
480
484
|
exit 1
|
481
485
|
end
|
482
486
|
|
487
|
+
ensure_service_exists!(service_name)
|
483
488
|
config = load_regolith_config
|
484
|
-
|
485
|
-
|
486
|
-
puts "❌ Error: Service '#{service_name}' not found"
|
487
|
-
exit 1
|
488
|
-
end
|
489
|
+
port = config['services'][service_name]['port']
|
490
|
+
url = "http://localhost:#{port}"
|
489
491
|
|
490
|
-
url = "http://localhost:#{service['port']}"
|
491
492
|
puts "🌐 Opening #{url}..."
|
492
493
|
|
494
|
+
# Cross-platform open command
|
493
495
|
case RbConfig::CONFIG['host_os']
|
494
496
|
when /mswin|mingw|cygwin/
|
495
497
|
system(%{start "" "#{url}"})
|
496
498
|
when /darwin/
|
497
|
-
system("open
|
498
|
-
when /linux|bsd/
|
499
|
-
system("xdg-open", url)
|
499
|
+
system("open #{url}")
|
500
500
|
else
|
501
|
-
|
501
|
+
system("xdg-open #{url}") || puts("Visit: #{url}")
|
502
502
|
end
|
503
503
|
end
|
504
504
|
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
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
|
513
517
|
end
|
514
|
-
elsif service_or_flag
|
515
|
-
run_single_db_command(service_or_flag, db_action)
|
516
518
|
else
|
517
|
-
puts "❌
|
518
|
-
puts "
|
519
|
+
puts "❌ Unknown db action: #{action}"
|
520
|
+
puts "Available: create, migrate, seed, reset, setup, drop"
|
519
521
|
exit 1
|
520
522
|
end
|
521
523
|
end
|
522
524
|
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
puts "
|
527
|
-
|
528
|
-
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')
|
529
530
|
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
531
|
+
unless success
|
532
|
+
puts "❌ Tests failed for #{service} (exit code: #{$?.exitstatus})"
|
533
|
+
exit 1 unless flags[:continue_on_failure]
|
534
|
+
end
|
534
535
|
end
|
535
|
-
|
536
|
+
|
537
|
+
puts "✅ All tests passed!" if flags[:all]
|
536
538
|
end
|
537
539
|
|
538
|
-
def
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
config['services'].each do |service_name, _|
|
543
|
-
puts "🧪 Running tests for #{service_name}..."
|
544
|
-
unless run_single_test(service_name)
|
545
|
-
all_passed = false
|
546
|
-
end
|
547
|
-
end
|
548
|
-
exit(1) unless all_passed
|
549
|
-
elsif service_or_flag
|
550
|
-
exit(1) unless run_single_test(service_or_flag)
|
551
|
-
else
|
552
|
-
puts "❌ Error: Service name or --all flag required"
|
553
|
-
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>"
|
554
544
|
exit 1
|
555
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')
|
556
550
|
end
|
557
551
|
|
558
|
-
|
552
|
+
# Health and monitoring
|
553
|
+
def health_check
|
554
|
+
puts "🔍 Health Check Results:"
|
555
|
+
puts
|
556
|
+
|
559
557
|
config = load_regolith_config
|
560
|
-
unless config['services'][service_name]
|
561
|
-
puts "❌ Error: Service '#{service_name}' not found"
|
562
|
-
return false
|
563
|
-
end
|
564
558
|
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
|
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
|
569
573
|
end
|
570
|
-
true
|
571
574
|
end
|
572
575
|
|
573
|
-
def
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
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
|
+
}
|
584
589
|
else
|
585
|
-
|
586
|
-
all_healthy = false
|
590
|
+
{ healthy: false, message: "HTTP #{response.code}" }
|
587
591
|
end
|
592
|
+
rescue
|
593
|
+
{ healthy: false, message: 'unreachable' }
|
588
594
|
end
|
589
|
-
|
590
|
-
exit(1) unless all_healthy
|
591
|
-
end
|
592
|
-
|
593
|
-
def check_service_health(service_name, port)
|
594
|
-
require_relative 'service_client'
|
595
|
-
response = Regolith::ServiceClient.health(service_name)
|
596
|
-
Regolith::ServiceClient.parsed_body(response) if response.code.to_i == 200
|
597
|
-
rescue => e
|
598
|
-
nil
|
599
595
|
end
|
600
596
|
|
601
|
-
|
597
|
+
# Configuration
|
598
|
+
def show_config(flags = {})
|
602
599
|
config = load_regolith_config
|
603
|
-
|
600
|
+
|
601
|
+
if flags[:json]
|
604
602
|
puts JSON.pretty_generate(config)
|
605
603
|
else
|
604
|
+
puts "📋 Current Configuration:"
|
605
|
+
puts
|
606
606
|
puts YAML.dump(config)
|
607
607
|
end
|
608
608
|
end
|
609
609
|
|
610
|
-
|
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
|
+
|
611
671
|
config = load_regolith_config
|
612
|
-
|
613
|
-
if
|
614
|
-
|
615
|
-
|
616
|
-
|
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
|
617
690
|
}
|
618
|
-
|
619
|
-
inspection[:endpoints][name] = "http://localhost:#{service['port']}"
|
620
|
-
end
|
621
|
-
puts JSON.pretty_generate(inspection)
|
691
|
+
puts JSON.pretty_generate(inspection_data)
|
622
692
|
else
|
623
|
-
|
624
|
-
|
625
|
-
|
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}"
|
626
698
|
end
|
627
|
-
|
628
|
-
puts "📋 Full Configuration:"
|
699
|
+
|
700
|
+
puts "\n📋 Full Configuration:"
|
629
701
|
puts YAML.dump(config)
|
630
702
|
end
|
631
703
|
end
|
632
704
|
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
# Check Docker Compose
|
645
|
-
if system("docker compose version > /dev/null 2>&1") ||
|
646
|
-
system("docker-compose --version > /dev/null 2>&1")
|
647
|
-
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]
|
648
716
|
else
|
649
|
-
puts "❌ Docker Compose
|
717
|
+
puts "❌ Docker Compose not found"
|
718
|
+
exit 1
|
650
719
|
end
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
|
658
|
-
|
659
|
-
|
660
|
-
|
661
|
-
|
662
|
-
|
663
|
-
|
664
|
-
|
665
|
-
|
666
|
-
|
667
|
-
|
668
|
-
|
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)
|
669
742
|
end
|
670
743
|
end
|
671
744
|
|
672
|
-
def
|
673
|
-
|
674
|
-
|
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
|
675
752
|
end
|
676
753
|
|
677
|
-
def
|
678
|
-
|
679
|
-
puts "
|
680
|
-
exec_compose("build", "--no-cache", service_name)
|
681
|
-
else
|
682
|
-
puts "🔨 Rebuilding all services..."
|
683
|
-
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']}"
|
684
757
|
end
|
758
|
+
|
759
|
+
puts "🧭 Service registry: config/regolith.yml"
|
760
|
+
puts
|
685
761
|
end
|
686
762
|
|
687
763
|
def load_regolith_config
|
688
|
-
config_path = find_regolith_config
|
689
|
-
return { 'services' => {} } unless config_path
|
690
|
-
|
764
|
+
config_path = Regolith.send(:find_regolith_config)
|
765
|
+
return { 'services' => {} } unless config_path && File.exist?(config_path)
|
766
|
+
|
767
|
+
# Use safe YAML loading
|
691
768
|
config = Psych.safe_load(File.read(config_path), permitted_classes: [], aliases: false) || {}
|
692
769
|
config['services'] ||= {}
|
693
770
|
config
|
694
771
|
end
|
695
772
|
|
696
|
-
def find_regolith_config
|
697
|
-
current_dir = Dir.pwd
|
698
|
-
while current_dir != '/'
|
699
|
-
config_path = File.join(current_dir, 'config', 'regolith.yml')
|
700
|
-
return config_path if File.exist?(config_path)
|
701
|
-
current_dir = File.dirname(current_dir)
|
702
|
-
end
|
703
|
-
nil
|
704
|
-
end
|
705
|
-
|
706
773
|
def save_regolith_config(config)
|
707
774
|
FileUtils.mkdir_p('config')
|
708
775
|
File.write('config/regolith.yml', YAML.dump(config))
|
776
|
+
Regolith.reload_service_registry!
|
709
777
|
end
|
710
778
|
|
711
779
|
def update_docker_compose(config)
|
@@ -713,82 +781,95 @@ module Regolith
|
|
713
781
|
File.write('docker-compose.yml', docker_compose)
|
714
782
|
end
|
715
783
|
|
784
|
+
# File generators
|
716
785
|
def generate_docker_compose(app_name, services = {})
|
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
|
-
|
773
|
-
|
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
|
774
848
|
end
|
775
849
|
|
776
850
|
def generate_dockerfile
|
777
851
|
<<~DOCKERFILE
|
778
|
-
FROM ruby:3.1
|
852
|
+
FROM ruby:3.1-slim
|
779
853
|
|
780
|
-
|
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/*
|
781
859
|
|
782
|
-
|
860
|
+
WORKDIR /app
|
783
861
|
|
862
|
+
# Copy Gemfile and install gems
|
784
863
|
COPY Gemfile Gemfile.lock* ./
|
785
864
|
|
865
|
+
# Conditional bundler config for dev vs prod
|
786
866
|
ARG BUILD_ENV=development
|
787
867
|
RUN if [ "$BUILD_ENV" = "production" ]; then \\
|
788
868
|
bundle config set --local deployment 'true' && \\
|
789
869
|
bundle config set --local without 'development test'; \\
|
790
870
|
fi && bundle install
|
791
871
|
|
872
|
+
# Copy application code
|
792
873
|
COPY . .
|
793
874
|
|
794
875
|
EXPOSE 3000
|
@@ -810,6 +891,7 @@ module Regolith
|
|
810
891
|
)
|
811
892
|
end
|
812
893
|
|
894
|
+
# Load service registry if available
|
813
895
|
if File.exist?(Rails.application.config.regolith.service_registry)
|
814
896
|
REGOLITH_SERVICES = YAML.load_file(Rails.application.config.regolith.service_registry)['services'] || {}
|
815
897
|
else
|
@@ -818,22 +900,184 @@ module Regolith
|
|
818
900
|
RUBY
|
819
901
|
end
|
820
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
|
+
|
821
1030
|
def generate_makefile
|
822
1031
|
<<~MAKEFILE
|
823
|
-
.PHONY: server console
|
1032
|
+
.PHONY: server up down restart logs console test health doctor
|
824
1033
|
|
825
|
-
|
1034
|
+
# Start services
|
1035
|
+
server up:
|
826
1036
|
\tregolith server
|
827
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)
|
828
1051
|
console:
|
829
|
-
\
|
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
|
830
1062
|
|
831
|
-
|
832
|
-
|
1063
|
+
# System diagnostics
|
1064
|
+
doctor:
|
1065
|
+
\tregolith doctor
|
833
1066
|
|
1067
|
+
# Database operations
|
1068
|
+
db-migrate:
|
1069
|
+
\tregolith db:migrate --all
|
1070
|
+
|
1071
|
+
db-setup:
|
1072
|
+
\tregolith db:setup --all
|
1073
|
+
|
1074
|
+
# Cleanup
|
834
1075
|
clean:
|
835
|
-
\
|
836
|
-
|
1076
|
+
\tregolith prune
|
1077
|
+
|
1078
|
+
# Shortcuts
|
1079
|
+
dev: up
|
1080
|
+
stop: down
|
837
1081
|
MAKEFILE
|
838
1082
|
end
|
839
1083
|
|
@@ -844,82 +1088,64 @@ module Regolith
|
|
844
1088
|
RUBY
|
845
1089
|
end
|
846
1090
|
|
847
|
-
def exec_compose(*args)
|
848
|
-
if system("docker compose version > /dev/null 2>&1")
|
849
|
-
exec("docker", "compose", *args)
|
850
|
-
elsif system("docker-compose --version > /dev/null 2>&1")
|
851
|
-
exec("docker-compose", *args)
|
852
|
-
else
|
853
|
-
puts "❌ Error: Neither 'docker compose' nor 'docker-compose' is available"
|
854
|
-
exit 1
|
855
|
-
end
|
856
|
-
end
|
857
|
-
|
858
|
-
def system_compose(*args)
|
859
|
-
if system("docker compose version > /dev/null 2>&1")
|
860
|
-
system("docker", "compose", *args)
|
861
|
-
elsif system("docker-compose --version > /dev/null 2>&1")
|
862
|
-
system("docker-compose", *args)
|
863
|
-
else
|
864
|
-
puts "❌ Error: Neither 'docker compose' nor 'docker-compose' is available"
|
865
|
-
false
|
866
|
-
end
|
867
|
-
end
|
868
|
-
|
869
1091
|
def show_help
|
870
1092
|
puts <<~HELP
|
871
|
-
Regolith #{Regolith::VERSION} - Rails
|
1093
|
+
Regolith #{Regolith::VERSION} - Rails for Distributed Systems
|
872
1094
|
|
873
1095
|
USAGE:
|
874
1096
|
regolith <command> [options]
|
875
1097
|
|
876
|
-
PROJECT
|
1098
|
+
PROJECT COMMANDS:
|
877
1099
|
new <app_name> Create a new Regolith application
|
878
1100
|
generate service <name> Generate a new microservice
|
879
1101
|
|
880
1102
|
SERVICE MANAGEMENT:
|
881
1103
|
server, up Start all services with Docker Compose
|
882
|
-
down Stop all services
|
883
|
-
restart [service] Restart
|
884
|
-
stop
|
885
|
-
ps, status Show
|
886
|
-
logs [service] [-f]
|
887
|
-
exec <service>
|
888
|
-
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
|
889
1112
|
|
890
1113
|
RAILS INTEGRATION:
|
891
|
-
rails <service> <command> Run Rails command in service
|
892
1114
|
console <service> Open Rails console for service
|
1115
|
+
rails <service> <cmd> Run Rails command in service
|
893
1116
|
routes <service> Show routes for service
|
894
|
-
open <service> Open service
|
1117
|
+
open <service> Open service in browser
|
895
1118
|
|
896
1119
|
DATABASE OPERATIONS:
|
897
|
-
db:
|
898
|
-
db:
|
899
|
-
db:seed [service|--all]
|
900
|
-
db:reset [service|--all]
|
901
|
-
db:setup [service|--all]
|
902
|
-
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
|
903
1126
|
|
904
1127
|
TESTING & HEALTH:
|
905
1128
|
test [service|--all] Run tests
|
906
|
-
health
|
1129
|
+
health Health check all services
|
907
1130
|
|
908
1131
|
UTILITIES:
|
909
|
-
config [--json] Show configuration
|
910
|
-
inspect [--json] Show
|
1132
|
+
config [--json] Show current configuration
|
1133
|
+
inspect [--json] Show services and resolved endpoints
|
1134
|
+
prune Clean up Docker system
|
911
1135
|
doctor Run system diagnostics
|
912
|
-
|
913
|
-
rebuild [service] Rebuild service images
|
914
|
-
version Show version information
|
1136
|
+
version Show version
|
915
1137
|
|
916
1138
|
EXAMPLES:
|
917
1139
|
regolith new marketplace
|
918
1140
|
regolith generate service products
|
919
1141
|
regolith server
|
920
1142
|
regolith rails products db:migrate
|
1143
|
+
regolith routes products
|
921
1144
|
regolith open products
|
922
|
-
regolith
|
1145
|
+
regolith shell products
|
1146
|
+
regolith inspect --json
|
1147
|
+
regolith config --json | jq '.services'
|
1148
|
+
regolith test --all
|
923
1149
|
|
924
1150
|
Get started:
|
925
1151
|
regolith new myapp
|