regolith 0.1.15 → 0.1.19
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 +711 -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)
|
286
316
|
|
287
|
-
#
|
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
|
332
|
+
|
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,89 @@ 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
|
-
volumes
|
770
|
-
postgres_data
|
771
|
-
|
772
|
-
|
773
|
-
ERB.new(template).result(binding)
|
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
|
+
db_name = "#{app_name}_#{name}_development" # ← per-service DB
|
808
|
+
|
809
|
+
compose_services[name] = {
|
810
|
+
'build' => {
|
811
|
+
'context' => service['root'],
|
812
|
+
'args' => { 'BUILD_ENV' => 'development' }
|
813
|
+
},
|
814
|
+
'ports' => ["#{service['port']}:3000"],
|
815
|
+
'networks' => ['regolith'],
|
816
|
+
'depends_on' => { 'db' => { 'condition' => 'service_healthy' } },
|
817
|
+
'environment' => {
|
818
|
+
'DATABASE_URL' => "postgres://postgres:password@db:5432/#{db_name}",
|
819
|
+
'REGOLITH_SERVICE_NAME' => name,
|
820
|
+
'REGOLITH_SERVICE_PORT' => service['port']
|
821
|
+
},
|
822
|
+
'volumes' => ["#{service['root']}:/app"],
|
823
|
+
'command' => 'bash -c "rm -f tmp/pids/server.pid && bundle install && rails db:prepare && rails server -b 0.0.0.0"',
|
824
|
+
'healthcheck' => {
|
825
|
+
'test' => ['CMD-SHELL', 'curl -f http://localhost:3000/health || exit 1'],
|
826
|
+
'interval' => '30s', 'timeout' => '10s', 'retries' => 3, 'start_period' => '40s'
|
827
|
+
}
|
828
|
+
}
|
829
|
+
end
|
830
|
+
|
831
|
+
|
832
|
+
{
|
833
|
+
'version' => '3.8',
|
834
|
+
'networks' => {
|
835
|
+
'regolith' => {}
|
836
|
+
},
|
837
|
+
'services' => compose_services,
|
838
|
+
'volumes' => {
|
839
|
+
'postgres_data' => nil
|
840
|
+
}
|
841
|
+
}.to_yaml
|
774
842
|
end
|
775
843
|
|
776
844
|
def generate_dockerfile
|
777
845
|
<<~DOCKERFILE
|
778
|
-
FROM ruby:3.1
|
846
|
+
FROM ruby:3.1-slim
|
779
847
|
|
780
|
-
|
848
|
+
# Install system dependencies
|
849
|
+
RUN apt-get update -qq && \\
|
850
|
+
apt-get install -y nodejs postgresql-client libyaml-dev build-essential pkg-config curl && \\
|
851
|
+
apt-get clean && \\
|
852
|
+
rm -rf /var/lib/apt/lists/*
|
781
853
|
|
782
|
-
|
854
|
+
WORKDIR /app
|
783
855
|
|
856
|
+
# Copy Gemfile and install gems
|
784
857
|
COPY Gemfile Gemfile.lock* ./
|
785
858
|
|
859
|
+
# Conditional bundler config for dev vs prod
|
786
860
|
ARG BUILD_ENV=development
|
787
861
|
RUN if [ "$BUILD_ENV" = "production" ]; then \\
|
788
862
|
bundle config set --local deployment 'true' && \\
|
789
863
|
bundle config set --local without 'development test'; \\
|
790
864
|
fi && bundle install
|
791
865
|
|
866
|
+
# Copy application code
|
792
867
|
COPY . .
|
793
868
|
|
794
869
|
EXPOSE 3000
|
@@ -810,6 +885,7 @@ module Regolith
|
|
810
885
|
)
|
811
886
|
end
|
812
887
|
|
888
|
+
# Load service registry if available
|
813
889
|
if File.exist?(Rails.application.config.regolith.service_registry)
|
814
890
|
REGOLITH_SERVICES = YAML.load_file(Rails.application.config.regolith.service_registry)['services'] || {}
|
815
891
|
else
|
@@ -818,22 +894,184 @@ module Regolith
|
|
818
894
|
RUBY
|
819
895
|
end
|
820
896
|
|
897
|
+
def generate_cors_initializer
|
898
|
+
<<~RUBY
|
899
|
+
# CORS configuration for microservices
|
900
|
+
Rails.application.config.middleware.insert_before 0, Rack::Cors do
|
901
|
+
allow do
|
902
|
+
origins '*' # Configure appropriately for production
|
903
|
+
resource '*',
|
904
|
+
headers: :any,
|
905
|
+
methods: %i[get post put patch delete options head],
|
906
|
+
expose: %w[Authorization Content-Type],
|
907
|
+
max_age: 600
|
908
|
+
end
|
909
|
+
end
|
910
|
+
RUBY
|
911
|
+
end
|
912
|
+
|
913
|
+
def generate_health_controller
|
914
|
+
<<~RUBY
|
915
|
+
module Regolith
|
916
|
+
class HealthController < ActionController::API
|
917
|
+
def show
|
918
|
+
render json: {
|
919
|
+
ok: true,
|
920
|
+
service: Rails.application.config.regolith_service_name,
|
921
|
+
time: Time.now.to_i,
|
922
|
+
version: Rails.application.config.regolith.version
|
923
|
+
}
|
924
|
+
end
|
925
|
+
end
|
926
|
+
end
|
927
|
+
RUBY
|
928
|
+
end
|
929
|
+
|
930
|
+
def generate_gitignore
|
931
|
+
<<~GITIGNORE
|
932
|
+
# Regolith
|
933
|
+
/services/*/log/*
|
934
|
+
/services/*/tmp/*
|
935
|
+
/services/*/.env*
|
936
|
+
.DS_Store
|
937
|
+
|
938
|
+
# Docker
|
939
|
+
docker-compose.override.yml
|
940
|
+
|
941
|
+
# Logs
|
942
|
+
*.log
|
943
|
+
|
944
|
+
# Runtime data
|
945
|
+
pids
|
946
|
+
*.pid
|
947
|
+
*.seed
|
948
|
+
|
949
|
+
# Environment variables
|
950
|
+
.env*
|
951
|
+
!.env.example
|
952
|
+
GITIGNORE
|
953
|
+
end
|
954
|
+
|
955
|
+
def generate_readme(app_name)
|
956
|
+
<<~MARKDOWN
|
957
|
+
# #{app_name.capitalize}
|
958
|
+
|
959
|
+
A Regolith microservices application built with Rails and Docker.
|
960
|
+
|
961
|
+
## Getting Started
|
962
|
+
|
963
|
+
```bash
|
964
|
+
# Start all services
|
965
|
+
regolith server
|
966
|
+
|
967
|
+
# Generate a new service
|
968
|
+
regolith generate service users
|
969
|
+
|
970
|
+
# Open service in browser
|
971
|
+
regolith open users
|
972
|
+
|
973
|
+
# View service logs
|
974
|
+
regolith logs users -f
|
975
|
+
|
976
|
+
# Run database migrations
|
977
|
+
regolith db:migrate --all
|
978
|
+
|
979
|
+
# Health check
|
980
|
+
regolith health
|
981
|
+
```
|
982
|
+
|
983
|
+
## Services
|
984
|
+
|
985
|
+
#{services_documentation}
|
986
|
+
|
987
|
+
## Development
|
988
|
+
|
989
|
+
```bash
|
990
|
+
# Open Rails console for a service
|
991
|
+
regolith console users
|
992
|
+
|
993
|
+
# Run Rails commands
|
994
|
+
regolith rails users db:migrate
|
995
|
+
regolith rails users routes
|
996
|
+
|
997
|
+
# Run tests
|
998
|
+
regolith test --all
|
999
|
+
|
1000
|
+
# Execute commands in service
|
1001
|
+
regolith exec users bash
|
1002
|
+
```
|
1003
|
+
|
1004
|
+
## Architecture
|
1005
|
+
|
1006
|
+
- **Rails 7** API-only applications
|
1007
|
+
- **PostgreSQL** for persistence
|
1008
|
+
- **Docker Compose** for orchestration
|
1009
|
+
- **Service registry** for inter-service communication
|
1010
|
+
|
1011
|
+
Built with [Regolith](https://regolith.bio) - Rails for distributed systems.
|
1012
|
+
MARKDOWN
|
1013
|
+
end
|
1014
|
+
|
1015
|
+
def services_documentation
|
1016
|
+
config = load_regolith_config
|
1017
|
+
return "No services yet. Run `regolith generate service <name>` to create one." if config['services'].empty?
|
1018
|
+
|
1019
|
+
config['services'].map do |name, service|
|
1020
|
+
"- **#{name}** - http://localhost:#{service['port']}"
|
1021
|
+
end.join("\n")
|
1022
|
+
end
|
1023
|
+
|
821
1024
|
def generate_makefile
|
822
1025
|
<<~MAKEFILE
|
823
|
-
.PHONY: server console
|
1026
|
+
.PHONY: server up down restart logs console test health doctor
|
824
1027
|
|
825
|
-
|
1028
|
+
# Start services
|
1029
|
+
server up:
|
826
1030
|
\tregolith server
|
827
1031
|
|
1032
|
+
# Stop services
|
1033
|
+
down:
|
1034
|
+
\tregolith down
|
1035
|
+
|
1036
|
+
# Restart services
|
1037
|
+
restart:
|
1038
|
+
\tregolith restart
|
1039
|
+
|
1040
|
+
# View logs
|
1041
|
+
logs:
|
1042
|
+
\tregolith logs -f
|
1043
|
+
|
1044
|
+
# Open console (usage: make console SERVICE=users)
|
828
1045
|
console:
|
829
|
-
\
|
1046
|
+
\t@if [ -z "$(SERVICE)" ]; then echo "Usage: make console SERVICE=service_name"; exit 1; fi
|
1047
|
+
\tregolith console $(SERVICE)
|
1048
|
+
|
1049
|
+
# Run tests
|
1050
|
+
test:
|
1051
|
+
\tregolith test --all
|
1052
|
+
|
1053
|
+
# Health check
|
1054
|
+
health:
|
1055
|
+
\tregolith health
|
830
1056
|
|
831
|
-
|
832
|
-
|
1057
|
+
# System diagnostics
|
1058
|
+
doctor:
|
1059
|
+
\tregolith doctor
|
833
1060
|
|
1061
|
+
# Database operations
|
1062
|
+
db-migrate:
|
1063
|
+
\tregolith db:migrate --all
|
1064
|
+
|
1065
|
+
db-setup:
|
1066
|
+
\tregolith db:setup --all
|
1067
|
+
|
1068
|
+
# Cleanup
|
834
1069
|
clean:
|
835
|
-
\
|
836
|
-
|
1070
|
+
\tregolith prune
|
1071
|
+
|
1072
|
+
# Shortcuts
|
1073
|
+
dev: up
|
1074
|
+
stop: down
|
837
1075
|
MAKEFILE
|
838
1076
|
end
|
839
1077
|
|
@@ -844,82 +1082,64 @@ module Regolith
|
|
844
1082
|
RUBY
|
845
1083
|
end
|
846
1084
|
|
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
1085
|
def show_help
|
870
1086
|
puts <<~HELP
|
871
|
-
Regolith #{Regolith::VERSION} - Rails
|
1087
|
+
Regolith #{Regolith::VERSION} - Rails for Distributed Systems
|
872
1088
|
|
873
1089
|
USAGE:
|
874
1090
|
regolith <command> [options]
|
875
1091
|
|
876
|
-
PROJECT
|
1092
|
+
PROJECT COMMANDS:
|
877
1093
|
new <app_name> Create a new Regolith application
|
878
1094
|
generate service <name> Generate a new microservice
|
879
1095
|
|
880
1096
|
SERVICE MANAGEMENT:
|
881
1097
|
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
|
1098
|
+
down Stop and remove all services
|
1099
|
+
restart [service] Restart one or all services
|
1100
|
+
stop [service] Stop one or all services
|
1101
|
+
ps, status Show running containers
|
1102
|
+
logs [service] [-f] View service logs
|
1103
|
+
exec <service> [cmd] Execute command in service container
|
1104
|
+
shell <service> Open shell in service container
|
1105
|
+
rebuild [service] Rebuild service images
|
889
1106
|
|
890
1107
|
RAILS INTEGRATION:
|
891
|
-
rails <service> <command> Run Rails command in service
|
892
1108
|
console <service> Open Rails console for service
|
1109
|
+
rails <service> <cmd> Run Rails command in service
|
893
1110
|
routes <service> Show routes for service
|
894
|
-
open <service> Open service
|
1111
|
+
open <service> Open service in browser
|
895
1112
|
|
896
1113
|
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]
|
1114
|
+
db:migrate [service|--all] Run migrations
|
1115
|
+
db:create [service|--all] Create databases
|
1116
|
+
db:seed [service|--all] Seed databases
|
1117
|
+
db:reset [service|--all] Reset databases
|
1118
|
+
db:setup [service|--all] Setup databases
|
1119
|
+
db:drop [service|--all] Drop databases
|
903
1120
|
|
904
1121
|
TESTING & HEALTH:
|
905
1122
|
test [service|--all] Run tests
|
906
|
-
health
|
1123
|
+
health Health check all services
|
907
1124
|
|
908
1125
|
UTILITIES:
|
909
|
-
config [--json] Show configuration
|
910
|
-
inspect [--json] Show
|
1126
|
+
config [--json] Show current configuration
|
1127
|
+
inspect [--json] Show services and resolved endpoints
|
1128
|
+
prune Clean up Docker system
|
911
1129
|
doctor Run system diagnostics
|
912
|
-
|
913
|
-
rebuild [service] Rebuild service images
|
914
|
-
version Show version information
|
1130
|
+
version Show version
|
915
1131
|
|
916
1132
|
EXAMPLES:
|
917
1133
|
regolith new marketplace
|
918
1134
|
regolith generate service products
|
919
1135
|
regolith server
|
920
1136
|
regolith rails products db:migrate
|
1137
|
+
regolith routes products
|
921
1138
|
regolith open products
|
922
|
-
regolith
|
1139
|
+
regolith shell products
|
1140
|
+
regolith inspect --json
|
1141
|
+
regolith config --json | jq '.services'
|
1142
|
+
regolith test --all
|
923
1143
|
|
924
1144
|
Get started:
|
925
1145
|
regolith new myapp
|