regolith 0.1.6 → 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,21 +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
24
76
  when 'restart'
25
- restart_services(@subcommand)
77
+ restart_service(@subcommand)
26
78
  when 'stop'
27
- stop_services
28
- when 'logs'
29
- show_logs(@subcommand)
30
- when 'status'
79
+ stop_service(@subcommand)
80
+ when 'ps', 'status'
31
81
  show_status
32
- when 'clean'
33
- clean_environment
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)
34
88
  when 'console'
35
89
  open_console(@subcommand)
36
- 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'
37
113
  puts "Regolith #{Regolith::VERSION}"
38
114
  else
39
115
  show_help
@@ -42,93 +118,21 @@ module Regolith
42
118
 
43
119
  private
44
120
 
45
- def restart_services(service_name = nil)
46
- unless File.exist?('docker-compose.yml')
47
- puts "❌ Error: Not in a Regolith app directory"
48
- exit 1
49
- end
50
-
51
- if service_name
52
- config = load_regolith_config
53
- unless config['services'][service_name]
54
- puts "❌ Error: Service '#{service_name}' not found"
55
- puts "Available services: #{config['services'].keys.join(', ')}"
56
- exit 1
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
57
133
  end
58
- puts "🔄 Restarting #{service_name} service..."
59
- system("docker-compose restart #{service_name}")
60
- else
61
- puts "🔄 Restarting all services..."
62
- system("docker-compose restart")
63
- end
64
- puts "✅ Restart complete"
65
- end
66
-
67
- def stop_services
68
- unless File.exist?('docker-compose.yml')
69
- puts "❌ Error: Not in a Regolith app directory"
70
- exit 1
71
- end
72
-
73
- puts "🛑 Stopping all services..."
74
- system("docker-compose down")
75
- puts "✅ All services stopped"
76
- end
77
-
78
- def show_logs(service_name)
79
- unless File.exist?('docker-compose.yml')
80
- puts "❌ Error: Not in a Regolith app directory"
81
- exit 1
82
- end
83
-
84
- unless service_name
85
- puts "❌ Error: Service name required"
86
- puts "Usage: regolith logs <service_name>"
87
- config = load_regolith_config
88
- puts "Available services: #{config['services'].keys.join(', ')}"
89
- exit 1
90
- end
91
-
92
- config = load_regolith_config
93
- unless config['services'][service_name]
94
- puts "❌ Error: Service '#{service_name}' not found"
95
- puts "Available services: #{config['services'].keys.join(', ')}"
96
- exit 1
97
- end
98
-
99
- puts "📋 Showing logs for #{service_name} (Press Ctrl+C to exit)..."
100
- exec("docker-compose logs -f #{service_name}")
101
- end
102
-
103
- def show_status
104
- unless File.exist?('docker-compose.yml')
105
- puts "❌ Error: Not in a Regolith app directory"
106
- exit 1
107
- end
108
-
109
- puts "📊 Regolith Service Status:"
110
- puts ""
111
-
112
- config = load_regolith_config
113
- puts "App: #{config['name']}"
114
- puts "Services: #{config['services'].size}"
115
- puts ""
116
-
117
- system("docker-compose ps")
118
- end
119
-
120
- def clean_environment
121
- unless File.exist?('docker-compose.yml')
122
- puts "❌ Error: Not in a Regolith app directory"
123
- exit 1
124
134
  end
125
-
126
- puts "🧹 Cleaning Docker environment..."
127
- puts " → Stopping and removing containers..."
128
- system("docker-compose down -v")
129
- puts " → Pruning unused Docker resources..."
130
- system("docker system prune -f")
131
- puts "✅ Environment cleaned"
135
+ flags
132
136
  end
133
137
 
134
138
  def create_new_app(app_name)
@@ -168,6 +172,8 @@ module Regolith
168
172
  File.write('docker-compose.yml', generate_docker_compose(app_name))
169
173
  File.write('Makefile', generate_makefile)
170
174
  File.write('.bin/regolith', generate_regolith_shim)
175
+ File.write('.gitignore', generate_gitignore)
176
+ File.write('README.md', generate_readme(app_name))
171
177
  FileUtils.chmod(0755, '.bin/regolith')
172
178
  end
173
179
 
@@ -182,9 +188,16 @@ module Regolith
182
188
  end
183
189
 
184
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
+
185
198
  puts "🔧 Creating service '#{service_name}'..."
186
199
  config = load_regolith_config
187
- port = 3001 + (config['services']&.size || 0)
200
+ port = next_available_port
188
201
  service_dir = "services/#{service_name}_service"
189
202
 
190
203
  puts " → Generating Rails API app..."
@@ -215,20 +228,60 @@ module Regolith
215
228
  exit 1
216
229
  end
217
230
 
218
- # Overwrite Gemfile before bundle install
219
- puts "🔧 Overwriting Gemfile to remove sqlite3 and other defaults..."
231
+ customize_service(service_dir, service_name, port)
232
+
233
+ config['services'][service_name] = {
234
+ 'port' => port,
235
+ 'root' => "./#{service_dir}"
236
+ }
220
237
 
221
- major_minor = `ruby -e 'print RUBY_VERSION.split(".")[0..1].join(".")'`.strip
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
252
+
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)
222
261
 
223
- custom_gemfile = <<~GEMFILE
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
224
276
  source "https://rubygems.org"
225
277
 
226
- ruby "~> #{major_minor}.0"
278
+ ruby "~> #{ruby_version}.0"
227
279
 
228
280
  gem "rails", "~> 7.2.2.1"
229
281
  gem "pg", "~> 1.5"
230
282
  gem "puma", ">= 5.0"
231
283
  gem "rack-cors"
284
+ gem "bootsnap", require: false
232
285
 
233
286
  group :development, :test do
234
287
  gem "debug", platforms: %i[ mri mswin mswin64 mingw x64_mingw ], require: "debug/prelude"
@@ -238,112 +291,491 @@ module Regolith
238
291
 
239
292
  gem "regolith", path: "vendor/regolith"
240
293
  GEMFILE
294
+ end
241
295
 
242
- File.write("#{service_dir}/Gemfile", custom_gemfile)
243
-
244
- # Vendor Regolith gem into service for Docker compatibility
296
+ def vendor_regolith_gem(service_dir)
245
297
  vendor_dir = File.join(service_dir, "vendor")
246
298
  FileUtils.mkdir_p(vendor_dir)
247
- FileUtils.cp_r(File.expand_path("../..", __dir__), File.join(vendor_dir, "regolith"))
248
- puts "📦 Vendored Regolith gem into #{File.join(service_dir, 'vendor', 'regolith')}"
249
-
250
- puts " → Running bundle install..."
251
- Dir.chdir(service_dir) do
252
- unless system("bundle install")
253
- puts "❌ bundle install failed"
254
- puts "→ You may be missing system libraries like libyaml-dev libsqlite3-dev build-essential pkg-config"
255
- puts "→ Try: sudo apt install -y libyaml-dev libsqlite3-dev build-essential pkg-config"
256
- exit 1
257
- end
258
- end
259
-
260
- patch_rails_app(service_dir, service_name, port)
261
-
262
- config['services'][service_name] = {
263
- 'port' => port,
264
- 'root' => "./#{service_dir}"
265
- }
266
- save_regolith_config(config)
267
- update_docker_compose(config)
268
-
269
- puts "✅ Created service '#{service_name}'"
270
- puts "🚀 Service running on port #{port}"
271
- puts "→ Next: regolith generate service <another_service> or regolith server"
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}"
272
305
  end
273
306
 
274
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
314
+
315
+ def create_initializers(service_dir, service_name)
275
316
  initializer_dir = "#{service_dir}/config/initializers"
276
317
  FileUtils.mkdir_p(initializer_dir)
318
+
277
319
  File.write("#{initializer_dir}/regolith.rb", generate_regolith_initializer(service_name))
278
- File.write("#{service_dir}/Dockerfile", generate_dockerfile)
320
+ File.write("#{initializer_dir}/cors.rb", generate_cors_initializer)
321
+ end
322
+
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'" }
340
+ end
341
+ end
279
342
 
343
+ def patch_application_rb(service_dir, service_name, port)
280
344
  app_rb_path = "#{service_dir}/config/application.rb"
281
345
  app_rb_content = File.read(app_rb_path)
282
346
 
283
347
  cors_config = <<~RUBY
284
348
 
285
349
  # Regolith configuration
286
- config.middleware.insert_before 0, Rack::Cors do
287
- allow do
288
- origins '*'
289
- resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head]
290
- end
291
- end
292
-
293
350
  config.regolith_service_name = '#{service_name}'
294
351
  config.regolith_service_port = #{port}
295
352
  RUBY
296
353
 
297
- app_rb_content.gsub!(/class Application < Rails::Application.*?
298
- end/m) do |match|
299
- match.gsub(/(\n end)$/, "#{cors_config}\1")
300
- end
301
-
354
+ app_rb_content.gsub!(/(\n end\n\z)/, "#{cors_config}\1")
302
355
  File.write(app_rb_path, app_rb_content)
303
356
  end
304
357
 
305
- def start_server
358
+ # Service management commands
359
+ def start_server(flags = {})
306
360
  unless File.exist?('docker-compose.yml')
307
361
  puts "❌ Error: Not in a Regolith app directory"
308
362
  exit 1
309
363
  end
310
364
 
311
365
  puts "🚀 Starting Regolith services..."
366
+
312
367
  config = load_regolith_config
368
+ show_service_info(config)
313
369
 
314
- config['services'].each do |name, service|
315
- puts "🚀 #{name}_service running at http://localhost:#{service['port']}"
370
+ exec_compose('up', '--build')
371
+ end
372
+
373
+ def stop_services
374
+ puts "🛑 Stopping all services..."
375
+ exec_compose('down', '-v')
376
+ end
377
+
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')
386
+ end
387
+ end
388
+
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')
316
397
  end
398
+ end
399
+
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
317
433
 
318
- puts "🧭 Service registry loaded from config/regolith.yml"
319
- puts ""
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)
440
+ end
320
441
 
321
- exec("docker-compose up --build")
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
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
458
+
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>"
471
+ exit 1
472
+ end
473
+
474
+ exec_compose('exec', service_name, 'bash', '-lc',
475
+ "bundle exec rails #{Shellwords.join(rails_args)}")
322
476
  end
323
477
 
324
478
  def open_console(service_name)
325
479
  unless service_name
326
480
  puts "❌ Error: Service name required"
327
481
  puts "Usage: regolith console <service_name>"
482
+ exit 1
483
+ end
484
+
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}")
512
+ end
513
+ end
514
+
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
534
+
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]
548
+ end
549
+
550
+ def show_routes(service_name)
551
+ unless service_name
552
+ puts "❌ Error: Service name required"
553
+ puts "Usage: regolith routes <service_name>"
554
+ exit 1
555
+ end
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 = {})
609
+ config = load_regolith_config
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')
328
665
  config = load_regolith_config
329
- puts "Available services: #{config['services'].keys.join(', ')}"
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"
330
728
  exit 1
331
729
  end
730
+ end
731
+
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
332
754
 
755
+ def ensure_service_exists!(service_name)
333
756
  config = load_regolith_config
334
- unless config['services'][service_name]
335
- puts "❌ Error: Service '#{service_name}' not found"
757
+ unless config['services'].key?(service_name)
758
+ puts "❌ Service '#{service_name}' not found"
336
759
  puts "Available services: #{config['services'].keys.join(', ')}"
337
760
  exit 1
338
761
  end
762
+ end
339
763
 
340
- puts "🧪 Opening Rails console for #{service_name}_service..."
341
- exec("docker-compose exec #{service_name} rails console")
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
342
771
  end
343
772
 
344
773
  def load_regolith_config
345
- return { 'services' => {} } unless File.exist?('config/regolith.yml')
346
- 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) || {}
347
779
  config['services'] ||= {}
348
780
  config
349
781
  end
@@ -351,6 +783,7 @@ module Regolith
351
783
  def save_regolith_config(config)
352
784
  FileUtils.mkdir_p('config')
353
785
  File.write('config/regolith.yml', YAML.dump(config))
786
+ Regolith.reload_service_registry!
354
787
  end
355
788
 
356
789
  def update_docker_compose(config)
@@ -358,56 +791,96 @@ module Regolith
358
791
  File.write('docker-compose.yml', docker_compose)
359
792
  end
360
793
 
794
+ # File generators
361
795
  def generate_docker_compose(app_name, services = {})
362
- template = <<~YAML
363
- version: '3.8'
364
-
365
- services:
366
- db:
367
- image: postgres:14
368
- environment:
369
- POSTGRES_DB: #{app_name}_development
370
- POSTGRES_USER: postgres
371
- POSTGRES_PASSWORD: password
372
- ports:
373
- - "5432:5432"
374
- volumes:
375
- - postgres_data:/var/lib/postgresql/data
376
-
377
- <% services.each do |name, service| %>
378
- <%= name %>:
379
- build: <%= service['root'] %>
380
- ports:
381
- - "<%= service['port'] %>:3000"
382
- depends_on:
383
- - db
384
- environment:
385
- DATABASE_URL: postgres://postgres:password@db:5432/<%= app_name %>_development
386
- REGOLITH_SERVICE_NAME: <%= name %>
387
- REGOLITH_SERVICE_PORT: <%= service['port'] %>
388
- volumes:
389
- - <%= service['root'] %>:/app
390
- command: bash -c "rm -f tmp/pids/server.pid && bundle install && rails server -b 0.0.0.0"
391
- <% end %>
392
-
393
- volumes:
394
- postgres_data:
395
- YAML
396
-
397
- 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
398
858
  end
399
859
 
400
860
  def generate_dockerfile
401
861
  <<~DOCKERFILE
402
- 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/*
403
869
 
404
870
  WORKDIR /app
405
871
 
406
- 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* ./
407
874
 
408
- 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
409
881
 
410
- RUN bundle install
882
+ # Copy application code
883
+ COPY . .
411
884
 
412
885
  EXPOSE 3000
413
886
 
@@ -423,10 +896,12 @@ module Regolith
423
896
  Rails.application.configure do
424
897
  config.regolith = OpenStruct.new(
425
898
  service_name: '#{service_name}',
426
- service_registry: Rails.root.join('../../config/regolith.yml')
899
+ service_registry: Rails.root.join('../../config/regolith.yml'),
900
+ version: Regolith::VERSION
427
901
  )
428
902
  end
429
903
 
904
+ # Load service registry if available
430
905
  if File.exist?(Rails.application.config.regolith.service_registry)
431
906
  REGOLITH_SERVICES = YAML.load_file(Rails.application.config.regolith.service_registry)['services'] || {}
432
907
  else
@@ -435,33 +910,184 @@ module Regolith
435
910
  RUBY
436
911
  end
437
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
+
438
1040
  def generate_makefile
439
1041
  <<~MAKEFILE
440
- .PHONY: server console build clean restart stop logs status
1042
+ .PHONY: server up down restart logs console test health doctor
441
1043
 
442
- server:
1044
+ # Start services
1045
+ server up:
443
1046
  regolith server
444
1047
 
1048
+ # Stop services
1049
+ down:
1050
+ regolith down
1051
+
1052
+ # Restart services
445
1053
  restart:
446
1054
  regolith restart
447
1055
 
448
- stop:
449
- regolith stop
450
-
1056
+ # View logs
451
1057
  logs:
452
- regolith logs
453
-
454
- status:
455
- regolith status
1058
+ regolith logs -f
456
1059
 
1060
+ # Open console (usage: make console SERVICE=users)
457
1061
  console:
458
- 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
1068
+
1069
+ # Health check
1070
+ health:
1071
+ regolith health
1072
+
1073
+ # System diagnostics
1074
+ doctor:
1075
+ regolith doctor
1076
+
1077
+ # Database operations
1078
+ db-migrate:
1079
+ regolith db:migrate --all
459
1080
 
460
- build:
461
- docker-compose build
1081
+ db-setup:
1082
+ regolith db:setup --all
462
1083
 
1084
+ # Cleanup
463
1085
  clean:
464
- regolith clean
1086
+ regolith prune
1087
+
1088
+ # Shortcuts
1089
+ dev: up
1090
+ stop: down
465
1091
  MAKEFILE
466
1092
  end
467
1093
 
@@ -474,38 +1100,62 @@ module Regolith
474
1100
 
475
1101
  def show_help
476
1102
  puts <<~HELP
477
- Regolith #{Regolith::VERSION} - Microservices framework
1103
+ Regolith #{Regolith::VERSION} - Rails for Distributed Systems
478
1104
 
479
1105
  USAGE:
480
1106
  regolith <command> [options]
481
1107
 
482
- COMMANDS:
1108
+ PROJECT COMMANDS:
483
1109
  new <app_name> Create a new Regolith application
484
1110
  generate service <name> Generate a new microservice
485
- server Start all services with Docker Compose
486
- restart [service_name] Restart all services or specific service
487
- stop Stop all services
488
- logs <service_name> Show logs for a service
489
- status Show status of all services
490
- clean Clean Docker environment
491
- console <service_name> Open Rails console for a service
492
- 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
493
1147
 
494
1148
  EXAMPLES:
495
- regolith new observatory
496
- regolith generate service telescope
1149
+ regolith new marketplace
1150
+ regolith generate service products
497
1151
  regolith server
498
- regolith restart element
499
- regolith logs element
500
- regolith status
501
- regolith console telescope
502
-
503
- SERVICE MANAGEMENT:
504
- regolith restart # Restart all services
505
- regolith restart element # Restart just element service
506
- regolith logs element # Follow element service logs
507
- regolith stop # Stop all services
508
- regolith clean # Clean containers and volumes
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
509
1159
 
510
1160
  Get started:
511
1161
  regolith new myapp