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,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "database/postgresql"
4
+ require_relative "database/mysql"
5
+ require_relative "database/sqlite"
6
+ require_relative "version_manager/mise"
7
+ require_relative "version_manager/rbenv"
8
+ require_relative "container/docker_compose"
9
+ require_relative "container/docker"
10
+
11
+ module Yoker
12
+ module Generators
13
+ class BaseGenerator
14
+ attr_reader :config, :shell
15
+
16
+ def initialize(config, shell)
17
+ @config = config
18
+ @shell = shell
19
+ end
20
+
21
+ def generate_all
22
+ generate_version_manager_files
23
+ generate_database_files
24
+ generate_container_files
25
+ generate_setup_script
26
+ update_gemfile if config[:database] != "sqlite3"
27
+ end
28
+
29
+ private
30
+
31
+ def generate_version_manager_files
32
+ case config[:version_manager]
33
+ when "mise"
34
+ VersionManager::Mise.new(config, shell).generate
35
+ when "rbenv"
36
+ VersionManager::Rbenv.new(config, shell).generate
37
+ end
38
+ end
39
+
40
+ def generate_database_files
41
+ case config[:database]
42
+ when "postgresql"
43
+ Database::Postgresql.new(config, shell).generate
44
+ when "mysql"
45
+ Database::Mysql.new(config, shell).generate
46
+ when "sqlite3"
47
+ Database::Sqlite.new(config, shell).generate
48
+ end
49
+ end
50
+
51
+ def generate_container_files
52
+ case config[:container]
53
+ when "docker-compose"
54
+ Container::DockerCompose.new(config, shell).generate
55
+ when "docker"
56
+ Container::Docker.new(config, shell).generate
57
+ when "none"
58
+ Container::None.new(config, shell).generate
59
+ end
60
+ end
61
+
62
+ def generate_setup_script
63
+ shell.template(
64
+ "bin/setup.rb.erb",
65
+ "bin/setup",
66
+ config
67
+ )
68
+
69
+ # Make executable
70
+ File.chmod(0o755, "bin/setup") if File.exist?("bin/setup")
71
+ end
72
+
73
+ def update_gemfile
74
+ gemfile_path = "Gemfile"
75
+ return unless File.exist?(gemfile_path)
76
+
77
+ gemfile_content = File.read(gemfile_path)
78
+
79
+ # Add database gem if not present
80
+ db_gem = database_gem_name
81
+ unless gemfile_content.include?(db_gem)
82
+ shell.append_to_file(gemfile_path, "\n# Database\ngem '#{db_gem}', '~> 1.1'\n")
83
+ end
84
+
85
+ # Add development gems
86
+ dev_gems = development_gems
87
+ dev_gems.each do |gem_name, version|
88
+ next if gemfile_content.include?(gem_name)
89
+
90
+ shell.append_to_file(
91
+ gemfile_path,
92
+ "\n# Development\ngem '#{gem_name}', '#{version}', group: :development\n"
93
+ )
94
+ end
95
+ end
96
+
97
+ def database_gem_name
98
+ case config[:database]
99
+ when "postgresql"
100
+ "pg"
101
+ when "mysql"
102
+ "mysql2"
103
+ when "sqlite3"
104
+ "sqlite3"
105
+ end
106
+ end
107
+
108
+ def development_gems
109
+ {
110
+ "debug" => "~> 1.8",
111
+ "letter_opener" => "~> 1.8"
112
+ }
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yoker
4
+ module Generators
5
+ module Container
6
+ class Docker
7
+ attr_reader :config, :shell
8
+
9
+ def initialize(config, shell)
10
+ @config = config
11
+ @shell = shell
12
+ end
13
+
14
+ def generate
15
+ generate_dockerfile if should_generate_dockerfile?
16
+ generate_dockerignore
17
+ generate_docker_scripts
18
+ generate_network_setup if needs_custom_network?
19
+ end
20
+
21
+ private
22
+
23
+ def generate_dockerfile
24
+ shell.template(
25
+ "docker/Dockerfile.erb",
26
+ "Dockerfile",
27
+ dockerfile_context
28
+ )
29
+ end
30
+
31
+ def generate_dockerignore
32
+ shell.template(
33
+ "docker/dockerignore.erb",
34
+ ".dockerignore",
35
+ {}
36
+ )
37
+ end
38
+
39
+ def generate_docker_scripts
40
+ # Generate convenience scripts for Docker management
41
+ generate_docker_dev_script
42
+ generate_docker_setup_script
43
+ end
44
+
45
+ def generate_docker_dev_script
46
+ shell.template(
47
+ "docker/dev.sh.erb",
48
+ "bin/docker-dev",
49
+ {
50
+ app_name: config[:app_name],
51
+ database: config[:database],
52
+ additional_services: config[:additional_services] || []
53
+ }
54
+ )
55
+
56
+ # Make executable
57
+ File.chmod(0o755, "bin/docker-dev") if File.exist?("bin/docker-dev")
58
+ end
59
+
60
+ def generate_docker_setup_script
61
+ shell.template(
62
+ "docker/docker-setup.sh.erb",
63
+ "bin/docker-setup",
64
+ docker_setup_context
65
+ )
66
+
67
+ # Make executable
68
+ File.chmod(0o755, "bin/docker-setup") if File.exist?("bin/docker-setup")
69
+ end
70
+
71
+ def generate_network_setup
72
+ # Create a custom Docker network for service communication
73
+ network_name = "#{config[:app_name]}_network"
74
+
75
+ shell.create_file(
76
+ "docker/network-setup.sh",
77
+ network_setup_script(network_name)
78
+ )
79
+
80
+ File.chmod(0o755, "docker/network-setup.sh") if File.exist?("docker/network-setup.sh")
81
+ end
82
+
83
+ def dockerfile_context
84
+ {
85
+ ruby_version: config[:ruby_version],
86
+ database: config[:database],
87
+ version_manager: config[:version_manager],
88
+ additional_packages: dockerfile_packages,
89
+ app_name: config[:app_name],
90
+ additional_services: config[:additional_services] || [],
91
+ needs_custom_entrypoint?: false # Standalone Docker doesn't need complex entrypoint
92
+ }
93
+ end
94
+
95
+ def docker_setup_context
96
+ {
97
+ app_name: config[:app_name],
98
+ database: config[:database],
99
+ database_image: database_image,
100
+ database_environment: database_environment,
101
+ database_ports: database_ports,
102
+ database_volumes: database_volumes,
103
+ database_health_check: database_health_check,
104
+ additional_containers: additional_containers,
105
+ network_name: "#{config[:app_name]}_network"
106
+ }
107
+ end
108
+
109
+ def dockerfile_packages
110
+ packages = %w[build-base git bash curl tzdata ca-certificates]
111
+
112
+ case config[:database]
113
+ when "postgresql"
114
+ packages << "postgresql-dev" << "postgresql-client"
115
+ when "mysql"
116
+ packages << "mysql-dev" << "mysql-client"
117
+ end
118
+
119
+ # Add Node.js if needed for assets
120
+ packages << "nodejs" << "npm"
121
+
122
+ packages
123
+ end
124
+
125
+ def database_image
126
+ case config[:database]
127
+ when "postgresql"
128
+ "postgres:18-alpine"
129
+ when "mysql"
130
+ "mysql:8.0"
131
+ when "sqlite3"
132
+ nil # SQLite doesn't need a container
133
+ end
134
+ end
135
+
136
+ def database_environment
137
+ case config[:database]
138
+ when "postgresql"
139
+ {
140
+ "POSTGRES_DB" => "#{config[:app_name]}_development",
141
+ "POSTGRES_USER" => "postgres",
142
+ "POSTGRES_PASSWORD" => "password"
143
+ }
144
+ when "mysql"
145
+ {
146
+ "MYSQL_ROOT_PASSWORD" => "password",
147
+ "MYSQL_DATABASE" => "#{config[:app_name]}_development",
148
+ "MYSQL_USER" => "rails",
149
+ "MYSQL_PASSWORD" => "password"
150
+ }
151
+ else
152
+ {}
153
+ end
154
+ end
155
+
156
+ def database_ports
157
+ case config[:database]
158
+ when "postgresql"
159
+ "5432:5432"
160
+ when "mysql"
161
+ "3306:3306"
162
+ else
163
+ nil
164
+ end
165
+ end
166
+
167
+ def database_volumes
168
+ container_name = "#{config[:app_name]}_#{config[:database]}"
169
+
170
+ case config[:database]
171
+ when "postgresql"
172
+ [
173
+ "#{container_name}_data:/var/lib/postgresql/data",
174
+ "./docker/init.sql:/docker-entrypoint-initdb.d/init.sql:ro"
175
+ ]
176
+ when "mysql"
177
+ [
178
+ "#{container_name}_data:/var/lib/mysql"
179
+ ]
180
+ else
181
+ []
182
+ end
183
+ end
184
+
185
+ def database_health_check
186
+ case config[:database]
187
+ when "postgresql"
188
+ "pg_isready -U postgres"
189
+ when "mysql"
190
+ "mysqladmin ping -h localhost -u root -ppassword"
191
+ else
192
+ nil
193
+ end
194
+ end
195
+
196
+ def additional_containers
197
+ containers = []
198
+
199
+ if config[:additional_services]&.include?("redis")
200
+ containers << {
201
+ name: "#{config[:app_name]}_redis",
202
+ image: "redis:7-alpine",
203
+ ports: "6379:6379",
204
+ volumes: ["#{config[:app_name]}_redis_data:/data"]
205
+ }
206
+ end
207
+
208
+ if config[:additional_services]&.include?("mailcatcher")
209
+ containers << {
210
+ name: "#{config[:app_name]}_mailcatcher",
211
+ image: "schickling/mailcatcher",
212
+ ports: "1080:1080 -p 1025:1025"
213
+ }
214
+ end
215
+
216
+ containers
217
+ end
218
+
219
+ def network_setup_script(network_name)
220
+ <<~BASH
221
+ #!/bin/bash
222
+ # Docker network setup for #{config[:app_name]}
223
+ # Generated by rails-dev-setup
224
+
225
+ set -e
226
+
227
+ NETWORK_NAME="#{network_name}"
228
+
229
+ echo "Setting up Docker network: $NETWORK_NAME"
230
+
231
+ # Create network if it doesn't exist
232
+ if ! docker network ls | grep -q "$NETWORK_NAME"; then
233
+ echo "Creating Docker network: $NETWORK_NAME"
234
+ docker network create "$NETWORK_NAME"
235
+ else
236
+ echo "Docker network $NETWORK_NAME already exists"
237
+ fi
238
+
239
+ echo "✅ Network setup complete"
240
+ BASH
241
+ end
242
+
243
+ def should_generate_dockerfile?
244
+ # Generate Dockerfile unless it already exists and user doesn't want to overwrite
245
+ !File.exist?("Dockerfile") || config[:force_overwrite]
246
+ end
247
+
248
+ def needs_custom_network?
249
+ # Custom network needed when we have multiple services
250
+ config[:additional_services]&.any? || config[:database] != "sqlite3"
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yoker
4
+ module Generators
5
+ module Container
6
+ class DockerCompose
7
+ attr_reader :config, :shell
8
+
9
+ def initialize(config, shell)
10
+ @config = config
11
+ @shell = shell
12
+ end
13
+
14
+ def generate
15
+ generate_compose_file
16
+ generate_dockerfile
17
+ generate_dockerignore
18
+ generate_entrypoint if needs_custom_entrypoint?
19
+ end
20
+
21
+ private
22
+
23
+ def generate_compose_file
24
+ services = base_services
25
+ services.merge!(additional_services)
26
+
27
+ shell.template(
28
+ "docker/docker-compose.yml.erb",
29
+ "docker-compose.yml",
30
+ {
31
+ services: services,
32
+ volumes: compose_volumes,
33
+ networks: compose_networks
34
+ }
35
+ )
36
+ end
37
+
38
+ def generate_dockerfile
39
+ shell.template(
40
+ "docker/Dockerfile.erb",
41
+ "Dockerfile",
42
+ {
43
+ ruby_version: config[:ruby_version],
44
+ database: config[:database],
45
+ version_manager: config[:version_manager],
46
+ additional_packages: dockerfile_packages
47
+ }
48
+ )
49
+ end
50
+
51
+ def generate_dockerignore
52
+ shell.template(
53
+ "docker/dockerignore.erb",
54
+ ".dockerignore",
55
+ {}
56
+ )
57
+ end
58
+
59
+ def generate_entrypoint
60
+ shell.template(
61
+ "docker/entrypoint.sh.erb",
62
+ "docker/entrypoint.sh",
63
+ config
64
+ )
65
+
66
+ File.chmod(0o755, "docker/entrypoint.sh") if File.exist?("docker/entrypoint.sh")
67
+ end
68
+
69
+ def base_services
70
+ services = {}
71
+
72
+ # Database service
73
+ services[:db] = database_service_config
74
+
75
+ # Web service
76
+ services[:web] = web_service_config
77
+
78
+ services
79
+ end
80
+
81
+ def database_service_config
82
+ case config[:database]
83
+ when "postgresql"
84
+ {
85
+ image: "postgres:18-alpine",
86
+ environment: {
87
+ "POSTGRES_DB" => "#{config[:app_name]}_development",
88
+ "POSTGRES_USER" => "postgres",
89
+ "POSTGRES_PASSWORD" => "password"
90
+ },
91
+ ports: ["5432:5432"],
92
+ volumes: [
93
+ "postgres_data:/var/lib/postgresql/data",
94
+ "./docker/init.sql:/docker-entrypoint-initdb.d/init.sql:ro"
95
+ ],
96
+ healthcheck: {
97
+ test: ["CMD-SHELL", "pg_isready -U postgres"],
98
+ interval: "10s",
99
+ timeout: "5s",
100
+ retries: 5
101
+ }
102
+ }
103
+ when "mysql"
104
+ {
105
+ image: "mysql:8.0",
106
+ environment: {
107
+ "MYSQL_ROOT_PASSWORD" => "password",
108
+ "MYSQL_DATABASE" => "#{config[:app_name]}_development",
109
+ "MYSQL_USER" => "rails",
110
+ "MYSQL_PASSWORD" => "password"
111
+ },
112
+ ports: ["3306:3306"],
113
+ volumes: [
114
+ "mysql_data:/var/lib/mysql"
115
+ ],
116
+ healthcheck: {
117
+ test: ["CMD", "mysqladmin", "ping", "-h", "localhost"],
118
+ interval: "10s",
119
+ timeout: "5s",
120
+ retries: 5
121
+ }
122
+ }
123
+ end
124
+ end
125
+
126
+ def web_service_config
127
+ depends_on = { db: { condition: "service_healthy" } }
128
+
129
+ # Add Redis dependency if included
130
+ depends_on[:redis] = { condition: "service_started" } if config[:additional_services]&.include?("redis")
131
+
132
+ {
133
+ build: ".",
134
+ command: 'bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -b \'0.0.0.0\'"',
135
+ volumes: [
136
+ ".:/rails",
137
+ "gem_cache:/usr/local/bundle"
138
+ ],
139
+ ports: ["3000:3000"],
140
+ depends_on: depends_on,
141
+ environment: web_environment,
142
+ stdin_open: true,
143
+ tty: true
144
+ }
145
+ end
146
+
147
+ def additional_services
148
+ services = {}
149
+
150
+ # Redis service
151
+ if config[:additional_services]&.include?("redis")
152
+ services[:redis] = {
153
+ image: "redis:7-alpine",
154
+ ports: ["6379:6379"],
155
+ volumes: ["redis_data:/data"]
156
+ }
157
+ end
158
+
159
+ # Sidekiq service
160
+ if config[:additional_services]&.include?("sidekiq")
161
+ services[:sidekiq] = {
162
+ build: ".",
163
+ command: "bundle exec sidekiq",
164
+ volumes: [
165
+ ".:/rails",
166
+ "gem_cache:/usr/local/bundle"
167
+ ],
168
+ depends_on: {
169
+ db: { condition: "service_healthy" },
170
+ redis: { condition: "service_started" }
171
+ },
172
+ environment: web_environment
173
+ }
174
+ end
175
+
176
+ # Mailcatcher service
177
+ if config[:additional_services]&.include?("mailcatcher")
178
+ services[:mailcatcher] = {
179
+ image: "schickling/mailcatcher",
180
+ ports: ["1080:1080", "1025:1025"]
181
+ }
182
+ end
183
+
184
+ services
185
+ end
186
+
187
+ def web_environment
188
+ env = {
189
+ "DATABASE_URL" => database_url,
190
+ "RAILS_ENV" => "development"
191
+ }
192
+
193
+ env["REDIS_URL"] = "redis://redis:6379/0" if config[:additional_services]&.include?("redis")
194
+
195
+ if config[:additional_services]&.include?("mailcatcher")
196
+ env["SMTP_HOST"] = "mailcatcher"
197
+ env["SMTP_PORT"] = "1025"
198
+ end
199
+
200
+ env
201
+ end
202
+
203
+ def database_url
204
+ case config[:database]
205
+ when "postgresql"
206
+ "postgresql://postgres:password@db:5432/#{config[:app_name]}_development"
207
+ when "mysql"
208
+ "mysql2://rails:password@db:3306/#{config[:app_name]}_development"
209
+ end
210
+ end
211
+
212
+ def compose_volumes
213
+ volumes = ["gem_cache"]
214
+
215
+ case config[:database]
216
+ when "postgresql"
217
+ volumes << "postgres_data"
218
+ when "mysql"
219
+ volumes << "mysql_data"
220
+ end
221
+
222
+ volumes << "redis_data" if config[:additional_services]&.include?("redis")
223
+
224
+ volumes
225
+ end
226
+
227
+ def compose_networks
228
+ {
229
+ default: {
230
+ driver: "bridge"
231
+ }
232
+ }
233
+ end
234
+
235
+ def dockerfile_packages
236
+ packages = %w[build-base git bash curl tzdata]
237
+
238
+ case config[:database]
239
+ when "postgresql"
240
+ packages << "postgresql-dev"
241
+ when "mysql"
242
+ packages << "mysql-dev"
243
+ end
244
+
245
+ packages << "nodejs" << "npm" # For asset pipeline
246
+ packages
247
+ end
248
+
249
+ def needs_custom_entrypoint?
250
+ config[:additional_services]&.any? || config[:database] != "sqlite3"
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end