yoker 0.1.0

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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +8 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +223 -0
  5. data/Rakefile +12 -0
  6. data/exe/yoker +177 -0
  7. data/exe/yoker (Copy) +87 -0
  8. data/lib/yoker/cli/base.rb +106 -0
  9. data/lib/yoker/cli/init.rb +193 -0
  10. data/lib/yoker/cli/update.rb +457 -0
  11. data/lib/yoker/configuration.rb +290 -0
  12. data/lib/yoker/detectors/database_detector.rb +35 -0
  13. data/lib/yoker/detectors/rails_detector.rb +48 -0
  14. data/lib/yoker/detectors/version_manager_detector.rb +91 -0
  15. data/lib/yoker/errors.rb +149 -0
  16. data/lib/yoker/generators/base_generator.rb +116 -0
  17. data/lib/yoker/generators/container/docker.rb +255 -0
  18. data/lib/yoker/generators/container/docker_compose.rb +255 -0
  19. data/lib/yoker/generators/container/none.rb +314 -0
  20. data/lib/yoker/generators/database/mysql.rb +147 -0
  21. data/lib/yoker/generators/database/postgresql.rb +64 -0
  22. data/lib/yoker/generators/database/sqlite.rb +123 -0
  23. data/lib/yoker/generators/version_manager/mise.rb +140 -0
  24. data/lib/yoker/generators/version_manager/rbenv.rb +165 -0
  25. data/lib/yoker/generators/version_manager/rvm.rb +246 -0
  26. data/lib/yoker/templates/bin/setup.rb.erb +232 -0
  27. data/lib/yoker/templates/config/database_mysql.yml.erb +47 -0
  28. data/lib/yoker/templates/config/database_postgresql.yml.erb +34 -0
  29. data/lib/yoker/templates/config/database_sqlite.yml.erb +40 -0
  30. data/lib/yoker/templates/docker/Dockerfile.erb +124 -0
  31. data/lib/yoker/templates/docker/docker-compose.yml.erb +117 -0
  32. data/lib/yoker/templates/docker/entrypoint.sh.erb +94 -0
  33. data/lib/yoker/templates/docker/init_mysql.sql.erb +44 -0
  34. data/lib/yoker/templates/docker/init_postgresql.sql.erb +203 -0
  35. data/lib/yoker/templates/version_managers/gemrc.erb +61 -0
  36. data/lib/yoker/templates/version_managers/mise.toml.erb +72 -0
  37. data/lib/yoker/templates/version_managers/rbenv_setup.sh.erb +93 -0
  38. data/lib/yoker/templates/version_managers/rvm_setup.sh.erb +99 -0
  39. data/lib/yoker/templates/version_managers/rvmrc.erb +70 -0
  40. data/lib/yoker/version.rb +5 -0
  41. data/lib/yoker.rb +32 -0
  42. data/sig/yoker.rbs +4 -0
  43. metadata +215 -0
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../generators/base_generator"
5
+
6
+ module Yoker
7
+ module CLI
8
+ class Init < Base
9
+ desc "generate", "Generate development environment setup"
10
+
11
+ option :database,
12
+ type: :string,
13
+ enum: %w[postgresql mysql sqlite3],
14
+ desc: "Database adapter to use"
15
+
16
+ option :version_manager,
17
+ type: :string,
18
+ enum: %w[mise rbenv rvm none],
19
+ desc: "Ruby version manager to use"
20
+
21
+ option :container,
22
+ type: :string,
23
+ enum: %w[docker-compose docker none],
24
+ desc: "Containerization approach"
25
+
26
+ option :ruby_version,
27
+ type: :string,
28
+ desc: "Ruby version to use"
29
+
30
+ option :interactive,
31
+ type: :boolean,
32
+ default: false,
33
+ aliases: %w[-i],
34
+ desc: "Run in interactive mode"
35
+
36
+ option :force,
37
+ type: :boolean,
38
+ default: false,
39
+ aliases: %w[-f],
40
+ desc: "Overwrite existing files"
41
+
42
+ def generate
43
+ detect_rails_app!
44
+
45
+ info "Setting up development environment for Rails application: #{current_directory_name}"
46
+
47
+ config = build_configuration
48
+
49
+ info "Configuration:"
50
+ display_configuration(config)
51
+
52
+ return if !options[:force] && !prompt.yes?("Continue with this configuration?")
53
+
54
+ spinner = spinner("Generating development setup files...")
55
+ spinner.auto_spin
56
+
57
+ begin
58
+ generator = Generators::BaseGenerator.new(config, self)
59
+ generator.generate_all
60
+
61
+ spinner.success("✅ Development setup complete!")
62
+
63
+ display_next_steps(config)
64
+ rescue StandardError => e
65
+ spinner.error("❌ Setup failed: #{e.message}")
66
+ puts e.backtrace.join("\n") if ENV["DEBUG"]
67
+ raise
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def build_configuration
74
+ config = {}
75
+
76
+ config = if options[:interactive]
77
+ interactive_configuration
78
+ else
79
+ options_configuration
80
+ end
81
+
82
+ # Fill in detected or default values
83
+ config[:app_name] ||= sanitize_name(current_directory_name)
84
+ config[:ruby_version] ||= RUBY_VERSION
85
+ config[:database] ||= detect_database
86
+ config[:version_manager] ||= detect_version_manager
87
+ config[:container] ||= detect_container_preference
88
+
89
+ config
90
+ end
91
+
92
+ def interactive_configuration
93
+ config = {}
94
+
95
+ config[:database] = prompt.select(
96
+ "Which database would you like to use?",
97
+ %w[postgresql mysql sqlite3],
98
+ default: detect_database
99
+ )
100
+
101
+ config[:version_manager] = prompt.select(
102
+ "Which Ruby version manager are you using?",
103
+ %w[mise rbenv rvm none],
104
+ default: detect_version_manager
105
+ )
106
+
107
+ config[:container] = prompt.select(
108
+ "How would you like to handle containerization?",
109
+ {
110
+ "Docker Compose (recommended)" => "docker-compose",
111
+ "Standalone Docker" => "docker",
112
+ "No containers" => "none"
113
+ }
114
+ )
115
+
116
+ config[:ruby_version] = prompt.ask(
117
+ "Ruby version to use?",
118
+ default: RUBY_VERSION
119
+ )
120
+
121
+ if config[:database] != "sqlite3"
122
+ config[:additional_services] = prompt.multi_select(
123
+ "Additional services to include?",
124
+ %w[redis sidekiq mailcatcher],
125
+ default: []
126
+ )
127
+ end
128
+
129
+ config
130
+ end
131
+
132
+ def options_configuration
133
+ {
134
+ database: options[:database],
135
+ version_manager: options[:version_manager],
136
+ container: options[:container],
137
+ ruby_version: options[:ruby_version]
138
+ }.compact
139
+ end
140
+
141
+ def detect_database
142
+ Detectors::DatabaseDetector.detect || "postgresql"
143
+ end
144
+
145
+ def detect_version_manager
146
+ Detectors::VersionManagerDetector.detect || "mise"
147
+ end
148
+
149
+ def detect_container_preference
150
+ return "docker-compose" if File.exist?("docker-compose.yml")
151
+ return "docker" if File.exist?("Dockerfile")
152
+
153
+ "docker-compose"
154
+ end
155
+
156
+ def display_configuration(config)
157
+ puts ""
158
+ puts pastel.cyan(" App name: ") + config[:app_name]
159
+ puts pastel.cyan(" Database: ") + config[:database]
160
+ puts pastel.cyan(" Ruby version: ") + config[:ruby_version]
161
+ puts pastel.cyan(" Version manager: ") + config[:version_manager]
162
+ puts pastel.cyan(" Containerization: ") + config[:container]
163
+
164
+ if config[:additional_services]&.any?
165
+ puts pastel.cyan(" Additional services: ") + config[:additional_services].join(", ")
166
+ end
167
+ puts ""
168
+ end
169
+
170
+ def display_next_steps(config)
171
+ puts ""
172
+ puts pastel.bright_green("🎉 Setup complete!")
173
+ puts ""
174
+ puts "Next steps:"
175
+ puts "1. Review the generated files"
176
+ puts "2. Run #{pastel.cyan("chmod +x bin/setup")} to make setup executable"
177
+ puts "3. Run #{pastel.cyan("./bin/setup")} to initialize your development environment"
178
+
179
+ if (config[:container] != "none") && (config[:container] == "docker-compose")
180
+ puts "4. Use #{pastel.cyan("docker compose up -d")} to start services"
181
+ end
182
+
183
+ puts ""
184
+ puts "Files generated:"
185
+ puts "• bin/setup - Enhanced setup script"
186
+ puts "• config/database.yml - Database configuration" if config[:database] != "sqlite3"
187
+ puts "• docker-compose.yml - Container orchestration" if config[:container] == "docker-compose"
188
+ puts "• Dockerfile - Container definition" if config[:container] != "none"
189
+ puts "• mise.toml - Version manager configuration" if config[:version_manager] == "mise"
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,457 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+ require_relative "../generators/base_generator"
5
+
6
+ module Yoker
7
+ module CLI
8
+ class Update < Base
9
+ desc "run", "Update existing development environment setup"
10
+
11
+ option :database,
12
+ type: :string,
13
+ enum: %w[postgresql mysql sqlite3],
14
+ desc: "Change database adapter"
15
+
16
+ option :version_manager,
17
+ type: :string,
18
+ enum: %w[mise rbenv rvm none],
19
+ desc: "Change Ruby version manager"
20
+
21
+ option :container,
22
+ type: :string,
23
+ enum: %w[docker-compose docker none],
24
+ desc: "Change containerization approach"
25
+
26
+ option :ruby_version,
27
+ type: :string,
28
+ desc: "Update Ruby version"
29
+
30
+ option :add_service,
31
+ type: :array,
32
+ enum: %w[redis sidekiq mailcatcher],
33
+ desc: "Add additional services"
34
+
35
+ option :remove_service,
36
+ type: :array,
37
+ enum: %w[redis sidekiq mailcatcher],
38
+ desc: "Remove services"
39
+
40
+ option :interactive,
41
+ type: :boolean,
42
+ default: false,
43
+ aliases: %w[-i],
44
+ desc: "Run in interactive mode"
45
+
46
+ option :backup,
47
+ type: :boolean,
48
+ default: true,
49
+ desc: "Backup existing files before updating"
50
+
51
+ option :dry_run,
52
+ type: :boolean,
53
+ default: false,
54
+ desc: "Show what would be changed without making changes"
55
+
56
+ def execute
57
+ detect_rails_app!
58
+
59
+ info "Updating development environment for Rails application: #{current_directory_name}"
60
+
61
+ current_config = detect_current_configuration
62
+ new_config = build_updated_configuration(current_config)
63
+
64
+ if configs_identical?(current_config, new_config)
65
+ success "No changes detected. Environment is up to date!"
66
+ return
67
+ end
68
+
69
+ display_configuration_changes(current_config, new_config)
70
+
71
+ if options[:dry_run]
72
+ info "Dry run mode - no changes made"
73
+ return
74
+ end
75
+
76
+ unless prompt.yes?("Apply these changes?")
77
+ info "Update cancelled"
78
+ return
79
+ end
80
+
81
+ perform_update(current_config, new_config)
82
+ end
83
+
84
+ private
85
+
86
+ def detect_current_configuration
87
+ config = {
88
+ app_name: sanitize_name(current_directory_name),
89
+ ruby_version: detect_current_ruby_version,
90
+ database: detect_current_database,
91
+ version_manager: detect_current_version_manager,
92
+ container: detect_current_container_setup,
93
+ additional_services: detect_current_services
94
+ }
95
+
96
+ info "Detected current configuration:"
97
+ display_simple_config(config)
98
+
99
+ config
100
+ end
101
+
102
+ def build_updated_configuration(current_config)
103
+ if options[:interactive]
104
+ interactive_update(current_config)
105
+ else
106
+ options_update(current_config)
107
+ end
108
+ end
109
+
110
+ def interactive_update(current_config)
111
+ new_config = current_config.dup
112
+
113
+ # Database update
114
+ if prompt.yes?("Change database adapter? (currently: #{current_config[:database]})")
115
+ new_config[:database] = prompt.select(
116
+ "Select new database:",
117
+ %w[postgresql mysql sqlite3],
118
+ default: current_config[:database]
119
+ )
120
+ end
121
+
122
+ # Version manager update
123
+ if prompt.yes?("Change version manager? (currently: #{current_config[:version_manager]})")
124
+ new_config[:version_manager] = prompt.select(
125
+ "Select version manager:",
126
+ %w[mise rbenv rvm none],
127
+ default: current_config[:version_manager]
128
+ )
129
+ end
130
+
131
+ # Container setup update
132
+ if prompt.yes?("Change containerization? (currently: #{current_config[:container]})")
133
+ new_config[:container] = prompt.select(
134
+ "Select containerization approach:",
135
+ {
136
+ "Docker Compose" => "docker-compose",
137
+ "Standalone Docker" => "docker",
138
+ "No containers" => "none"
139
+ },
140
+ default: current_config[:container]
141
+ )
142
+ end
143
+
144
+ # Ruby version update
145
+ if prompt.yes?("Update Ruby version? (currently: #{current_config[:ruby_version]})")
146
+ new_config[:ruby_version] = prompt.ask(
147
+ "Enter Ruby version:",
148
+ default: current_config[:ruby_version]
149
+ )
150
+ end
151
+
152
+ # Services update
153
+ if prompt.yes?("Modify additional services?")
154
+ current_services = current_config[:additional_services] || []
155
+ new_config[:additional_services] = prompt.multi_select(
156
+ "Select services:",
157
+ %w[redis sidekiq mailcatcher],
158
+ default: current_services
159
+ )
160
+ end
161
+
162
+ new_config
163
+ end
164
+
165
+ def options_update(current_config)
166
+ new_config = current_config.dup
167
+
168
+ # Apply command line options
169
+ new_config[:database] = options[:database] if options[:database]
170
+ new_config[:version_manager] = options[:version_manager] if options[:version_manager]
171
+ new_config[:container] = options[:container] if options[:container]
172
+ new_config[:ruby_version] = options[:ruby_version] if options[:ruby_version]
173
+
174
+ # Handle service additions/removals
175
+ if options[:add_service] || options[:remove_service]
176
+ services = Set.new(current_config[:additional_services] || [])
177
+
178
+ services.merge(options[:add_service]) if options[:add_service]
179
+
180
+ services.subtract(options[:remove_service]) if options[:remove_service]
181
+
182
+ new_config[:additional_services] = services.to_a
183
+ end
184
+
185
+ new_config
186
+ end
187
+
188
+ def perform_update(current_config, new_config)
189
+ backup_existing_files if options[:backup]
190
+
191
+ spinner = spinner("Updating development environment...")
192
+ spinner.auto_spin
193
+
194
+ begin
195
+ # Handle major changes that require cleanup
196
+ handle_container_migration(current_config, new_config)
197
+ handle_database_migration(current_config, new_config)
198
+ handle_version_manager_migration(current_config, new_config)
199
+
200
+ # Generate updated configuration
201
+ generator = Generators::BaseGenerator.new(new_config, self)
202
+ generator.generate_all
203
+
204
+ # Update existing services
205
+ update_running_services(current_config, new_config)
206
+
207
+ spinner.success("✅ Environment updated successfully!")
208
+
209
+ display_post_update_instructions(current_config, new_config)
210
+ rescue StandardError => e
211
+ spinner.error("❌ Update failed: #{e.message}")
212
+ offer_rollback if options[:backup]
213
+ raise
214
+ end
215
+ end
216
+
217
+ def handle_container_migration(old_config, new_config)
218
+ return if old_config[:container] == new_config[:container]
219
+
220
+ info "Migrating containerization from #{old_config[:container]} to #{new_config[:container]}"
221
+
222
+ case [old_config[:container], new_config[:container]]
223
+ when %w[docker-compose docker]
224
+ migrate_compose_to_docker
225
+ when %w[docker-compose none]
226
+ migrate_compose_to_native
227
+ when %w[docker docker-compose]
228
+ migrate_docker_to_compose
229
+ when %w[docker none]
230
+ migrate_docker_to_native
231
+ when %w[none docker-compose]
232
+ migrate_native_to_compose
233
+ when %w[none docker]
234
+ migrate_native_to_docker
235
+ end
236
+ end
237
+
238
+ def handle_database_migration(old_config, new_config)
239
+ return if old_config[:database] == new_config[:database]
240
+
241
+ warning "Database change detected: #{old_config[:database]} → #{new_config[:database]}"
242
+ warning "You will need to migrate your data manually"
243
+
244
+ info "Steps to migrate your database:"
245
+ puts "1. Export data from #{old_config[:database]}"
246
+ puts "2. Run './bin/setup' to initialize #{new_config[:database]}"
247
+ puts "3. Import your data to the new database"
248
+ end
249
+
250
+ def handle_version_manager_migration(old_config, new_config)
251
+ return if old_config[:version_manager] == new_config[:version_manager]
252
+
253
+ info "Migrating version manager: #{old_config[:version_manager]} → #{new_config[:version_manager]}"
254
+
255
+ # Clean up old version manager files
256
+ cleanup_version_manager_files(old_config[:version_manager])
257
+ end
258
+
259
+ def cleanup_version_manager_files(old_manager)
260
+ files_to_remove = case old_manager
261
+ when "mise"
262
+ %w[mise.toml .mise.local.toml]
263
+ when "rbenv"
264
+ %w[.ruby-version]
265
+ when "rvm"
266
+ %w[.rvmrc]
267
+ end
268
+
269
+ files_to_remove&.each do |file|
270
+ if File.exist?(file)
271
+ info "Removing #{file}"
272
+ File.delete(file) unless options[:dry_run]
273
+ end
274
+ end
275
+ end
276
+
277
+ def update_running_services(old_config, new_config)
278
+ return unless old_config[:container] != "none" && new_config[:container] != "none"
279
+
280
+ # Stop old services
281
+ stop_services(old_config)
282
+
283
+ # Start new services
284
+ start_services(new_config)
285
+ end
286
+
287
+ def stop_services(config)
288
+ case config[:container]
289
+ when "docker-compose"
290
+ system("docker compose down", exception: false) if File.exist?("docker-compose.yml")
291
+ when "docker"
292
+ stop_standalone_containers(config)
293
+ end
294
+ end
295
+
296
+ def start_services(config)
297
+ info "Starting services with new configuration..."
298
+ system("./bin/setup", exception: false) if File.exist?("bin/setup")
299
+ end
300
+
301
+ def stop_standalone_containers(config)
302
+ container_name = "#{config[:app_name]}_#{config[:database]}"
303
+ system("docker stop #{container_name}", exception: false)
304
+
305
+ return unless config[:additional_services]&.include?("redis")
306
+
307
+ redis_container = "#{config[:app_name]}_redis"
308
+ system("docker stop #{redis_container}", exception: false)
309
+ end
310
+
311
+ def backup_existing_files
312
+ timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
313
+ backup_dir = "backup_#{timestamp}"
314
+
315
+ info "Creating backup in #{backup_dir}/"
316
+ Dir.mkdir(backup_dir)
317
+
318
+ files_to_backup.each do |file|
319
+ if File.exist?(file)
320
+ backup_path = File.join(backup_dir, file.gsub("/", "_"))
321
+ FileUtils.cp(file, backup_path)
322
+ end
323
+ end
324
+ end
325
+
326
+ def files_to_backup
327
+ %w[
328
+ bin/setup
329
+ config/database.yml
330
+ docker-compose.yml
331
+ Dockerfile
332
+ mise.toml
333
+ .ruby-version
334
+ .tool-versions
335
+ Procfile.dev
336
+ ]
337
+ end
338
+
339
+ def offer_rollback
340
+ return unless prompt.yes?("Restore from backup?")
341
+
342
+ # Implementation for rollback would go here
343
+ info "Rollback functionality coming soon"
344
+ end
345
+
346
+ def configs_identical?(config1, config2)
347
+ config1.to_s == config2.to_s
348
+ end
349
+
350
+ def display_configuration_changes(old_config, new_config)
351
+ puts ""
352
+ puts pastel.bright_blue("Configuration Changes:")
353
+ puts ""
354
+
355
+ old_config.each do |key, old_value|
356
+ new_value = new_config[key]
357
+ next if old_value == new_value
358
+
359
+ puts " #{pastel.cyan(key.to_s)}:"
360
+ puts " #{pastel.red("- #{old_value}")}"
361
+ puts " #{pastel.green("+ #{new_value}")}"
362
+ end
363
+ puts ""
364
+ end
365
+
366
+ def display_simple_config(config)
367
+ config.each do |key, value|
368
+ puts " #{pastel.cyan(key)}: #{value}"
369
+ end
370
+ puts ""
371
+ end
372
+
373
+ def display_post_update_instructions(old_config, new_config)
374
+ puts ""
375
+ puts pastel.bright_green("🎉 Update complete!")
376
+ puts ""
377
+ puts "Next steps:"
378
+ puts "1. Review the updated configuration files"
379
+ puts "2. Run #{pastel.cyan("./bin/setup")} to apply changes"
380
+
381
+ if old_config[:database] != new_config[:database]
382
+ puts "3. #{pastel.yellow("Important:")} Migrate your database data manually"
383
+ end
384
+
385
+ return unless old_config[:container] != new_config[:container]
386
+
387
+ puts "4. Update your development workflow for the new container setup"
388
+ end
389
+
390
+ # Detection methods
391
+ def detect_current_ruby_version
392
+ Detectors::VersionManagerDetector.ruby_version_from_file || RUBY_VERSION
393
+ end
394
+
395
+ def detect_current_database
396
+ Detectors::DatabaseDetector.detect || "sqlite3"
397
+ end
398
+
399
+ def detect_current_version_manager
400
+ Detectors::VersionManagerDetector.detect || "none"
401
+ end
402
+
403
+ def detect_current_container_setup
404
+ return "docker-compose" if File.exist?("docker-compose.yml")
405
+ return "docker" if File.exist?("Dockerfile") && !File.exist?("docker-compose.yml")
406
+
407
+ "none"
408
+ end
409
+
410
+ def detect_current_services
411
+ services = []
412
+
413
+ if File.exist?("docker-compose.yml")
414
+ compose_content = File.read("docker-compose.yml")
415
+ services << "redis" if compose_content.include?("redis:")
416
+ services << "sidekiq" if compose_content.include?("sidekiq:")
417
+ services << "mailcatcher" if compose_content.include?("mailcatcher")
418
+ end
419
+
420
+ services << "sidekiq" if File.exist?("config/sidekiq.yml") && !services.include?("sidekiq")
421
+
422
+ services
423
+ end
424
+
425
+ # Container migration methods
426
+ def migrate_compose_to_docker
427
+ info "Migrating from Docker Compose to standalone Docker"
428
+ system("docker compose down", exception: false) if File.exist?("docker-compose.yml")
429
+ end
430
+
431
+ def migrate_compose_to_native
432
+ info "Migrating from Docker Compose to native setup"
433
+ system("docker compose down", exception: false) if File.exist?("docker-compose.yml")
434
+ end
435
+
436
+ def migrate_docker_to_compose
437
+ info "Migrating from standalone Docker to Docker Compose"
438
+ # Stop existing containers
439
+ end
440
+
441
+ def migrate_docker_to_native
442
+ info "Migrating from standalone Docker to native setup"
443
+ # Stop existing containers
444
+ end
445
+
446
+ def migrate_native_to_compose
447
+ info "Migrating from native setup to Docker Compose"
448
+ # Stop native services if possible
449
+ end
450
+
451
+ def migrate_native_to_docker
452
+ info "Migrating from native setup to standalone Docker"
453
+ # Stop native services if possible
454
+ end
455
+ end
456
+ end
457
+ end