regolith 0.1.5 → 0.1.7

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