railsmaker-core 0.0.1

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 (59) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +169 -0
  3. data/bin/railsmaker +344 -0
  4. data/lib/railsmaker/generators/app_generator.rb +157 -0
  5. data/lib/railsmaker/generators/auth_generator.rb +97 -0
  6. data/lib/railsmaker/generators/base_generator.rb +39 -0
  7. data/lib/railsmaker/generators/concerns/gsub_validation.rb +24 -0
  8. data/lib/railsmaker/generators/litestream_generator.rb +89 -0
  9. data/lib/railsmaker/generators/mailjet_generator.rb +68 -0
  10. data/lib/railsmaker/generators/opentelemetry_generator.rb +79 -0
  11. data/lib/railsmaker/generators/plausible_generator.rb +33 -0
  12. data/lib/railsmaker/generators/plausible_instrumentation_generator.rb +28 -0
  13. data/lib/railsmaker/generators/sentry_generator.rb +46 -0
  14. data/lib/railsmaker/generators/server_command_generator.rb +178 -0
  15. data/lib/railsmaker/generators/signoz_generator.rb +33 -0
  16. data/lib/railsmaker/generators/signoz_opentelemetry_generator.rb +37 -0
  17. data/lib/railsmaker/generators/templates/app/credentials.example.yml +14 -0
  18. data/lib/railsmaker/generators/templates/app/main_index.html.erb +71 -0
  19. data/lib/railsmaker/generators/templates/auth/app/controllers/omniauth_callbacks_controller.rb +18 -0
  20. data/lib/railsmaker/generators/templates/litestream/litestream.yml.erb +32 -0
  21. data/lib/railsmaker/generators/templates/opentelemetry/lograge.rb.erb +9 -0
  22. data/lib/railsmaker/generators/templates/shell_scripts/plausible.sh.erb +51 -0
  23. data/lib/railsmaker/generators/templates/shell_scripts/signoz.sh.erb +38 -0
  24. data/lib/railsmaker/generators/templates/shell_scripts/signoz_opentelemetry.sh.erb +49 -0
  25. data/lib/railsmaker/generators/templates/ui/app/assets/images/og-image.webp +0 -0
  26. data/lib/railsmaker/generators/templates/ui/app/assets/images/plausible-screenshot.png +0 -0
  27. data/lib/railsmaker/generators/templates/ui/app/assets/images/signoz-screenshot.png +0 -0
  28. data/lib/railsmaker/generators/templates/ui/app/controllers/demo_controller.rb +7 -0
  29. data/lib/railsmaker/generators/templates/ui/app/controllers/pages_controller.rb +4 -0
  30. data/lib/railsmaker/generators/templates/ui/app/helpers/seo_helper.rb +39 -0
  31. data/lib/railsmaker/generators/templates/ui/app/javascript/controllers/flash_controller.js +14 -0
  32. data/lib/railsmaker/generators/templates/ui/app/javascript/controllers/index.js +11 -0
  33. data/lib/railsmaker/generators/templates/ui/app/javascript/controllers/scroll_fade_controller.js +27 -0
  34. data/lib/railsmaker/generators/templates/ui/app/views/clearance_mailer/change_password.html.erb +8 -0
  35. data/lib/railsmaker/generators/templates/ui/app/views/clearance_mailer/change_password.text.erb +5 -0
  36. data/lib/railsmaker/generators/templates/ui/app/views/demo/analytics.html.erb +214 -0
  37. data/lib/railsmaker/generators/templates/ui/app/views/demo/index.html.erb +312 -0
  38. data/lib/railsmaker/generators/templates/ui/app/views/demo/support.html.erb +147 -0
  39. data/lib/railsmaker/generators/templates/ui/app/views/layouts/_navbar.html.erb +193 -0
  40. data/lib/railsmaker/generators/templates/ui/app/views/layouts/application.html.erb +62 -0
  41. data/lib/railsmaker/generators/templates/ui/app/views/main/index.html.erb +320 -0
  42. data/lib/railsmaker/generators/templates/ui/app/views/pages/privacy.html.erb +63 -0
  43. data/lib/railsmaker/generators/templates/ui/app/views/pages/terms.html.erb +54 -0
  44. data/lib/railsmaker/generators/templates/ui/app/views/passwords/create.html.erb +9 -0
  45. data/lib/railsmaker/generators/templates/ui/app/views/passwords/edit.html.erb +21 -0
  46. data/lib/railsmaker/generators/templates/ui/app/views/passwords/new.html.erb +26 -0
  47. data/lib/railsmaker/generators/templates/ui/app/views/sessions/new.html.erb +49 -0
  48. data/lib/railsmaker/generators/templates/ui/app/views/shared/_auth_layout.html.erb +24 -0
  49. data/lib/railsmaker/generators/templates/ui/app/views/shared/_flash.html.erb +19 -0
  50. data/lib/railsmaker/generators/templates/ui/app/views/shared/_footer.html.erb +52 -0
  51. data/lib/railsmaker/generators/templates/ui/app/views/shared/_structured_data.html.erb +20 -0
  52. data/lib/railsmaker/generators/templates/ui/app/views/users/new.html.erb +49 -0
  53. data/lib/railsmaker/generators/templates/ui/config/sitemap.rb +33 -0
  54. data/lib/railsmaker/generators/templates/ui/public/icon.png +0 -0
  55. data/lib/railsmaker/generators/templates/ui/public/icon.svg +5 -0
  56. data/lib/railsmaker/generators/templates/ui/public/robots.txt +8 -0
  57. data/lib/railsmaker/generators/ui_generator.rb +68 -0
  58. data/lib/railsmaker.rb +29 -0
  59. metadata +359 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b0ed25a62bb449bb3710dafb3d4cc089df97477d7b6439df3f8d8b1b0b6efbbd
4
+ data.tar.gz: d128667d1ef969f12940bd0f84a8081753afa2ab33432341564a746a5fb4169f
5
+ SHA512:
6
+ metadata.gz: e503c24961bfe8c4919dfafe60ee4910c304f9d9145a179b42f02cbf7bef3564b0bc3b2efdc3d3f3dd4a24841ebf761b1b33dd061c7f7d7d40e27d68f2270b7e
7
+ data.tar.gz: de25342c872d7c3a49c5035fe98f90935a9fa31bde6954bbe28245c169ae0f9941fc9a7a9c83db42955dd98c346d9566cbe12e0ec15d069b459227dd4a9464f5
data/README.md ADDED
@@ -0,0 +1,169 @@
1
+ [![Gem Version](https://img.shields.io/gem/v/railsmaker?color=blue&logo=rubygems)](https://rubygems.org/gems/railsmaker)
2
+ [![Support](https://img.shields.io/badge/Support-%F0%9F%8D%B8-yellow)](https://buymeacoffee.com/sgerov)
3
+ [![Live Demo](https://img.shields.io/badge/Live_Demo-Try_Now_-brightgreen?logo=rocket&color=00cc99)](https://railsmaker.com)
4
+ [![Live Demo Repo](https://img.shields.io/badge/Live_Demo_Repo-View_Code-blue?logo=github)](https://github.com/sgerov/railsmaker-sample)
5
+ [![Guide](https://img.shields.io/badge/Guide-10_Steps_To_Prod-orange?logo=book)](./10-STEPS-TO-PROD.md)
6
+
7
+ # 📦 Railsmaker
8
+
9
+ Ship your MVP in hours, not weeks • Zero config needed • Save $120+/month and 20+ dev hours
10
+
11
+ ## ⚡ Why Railsmaker?
12
+ - **Ship Faster**: From zero to production in 15 minutes
13
+ - **Growth Ready**: Built-in analytics, SEO, and monitoring
14
+ - **Own Your Data**: Fully self-hosted, full control, full flexibility
15
+ - **Cost Efficient**: You decide how much you want to spend
16
+ - **DX Focused**: Modern stack, zero configuration
17
+
18
+ ## ✨ Features
19
+
20
+ #### Growth & Analytics
21
+ - **Privacy-focused**: Self-hosted Plausible and Signoz
22
+ - **SEO**: Auto-optimized meta-tags & sitemaps
23
+ - **Performance**: Lightning-fast ~50ms page loads
24
+ - **Mobile First**: Instant responsive layouts
25
+
26
+ #### Developer Experience
27
+ - **UI**: Latest TailwindCSS 4 + DaisyUI 5
28
+ - **Auth**: Battle-tested Clearance + OmniAuth
29
+ - **Storage**: SQLite + Litestream
30
+ - **Email**: Production-ready Mailjet integration
31
+ - **Modern Stack**: Rails 8, Ruby 3.2, Hotwire magic
32
+
33
+ #### Infrastructure
34
+ - **Monitoring**: Full SigNoz & Sentry integration
35
+ - **Deploy**: One-command Kamal deployments
36
+ - **Observability**: Enterprise-grade OpenTelemetry + Lograge
37
+ - **Scale-ready**: Global CDN support, multi-environment
38
+
39
+ ## 🚀 Setup
40
+
41
+ ### Prerequisites
42
+ - Ruby 3.x (`rbenv` or `rvm` recommended)
43
+ - Bundler: `gem install bundler`
44
+ - Bun: [Install guide](https://bun.sh)
45
+ - Git
46
+ - Dev tools:
47
+ - Ubuntu/Debian: `sudo apt install build-essential libyaml-dev`
48
+ - macOS: `xcode-select --install`
49
+ - Docker (for analytics & monitoring)
50
+
51
+ ### 1. Bootstrapping your app
52
+
53
+ #### A. Set Required Environment Variables
54
+
55
+ ```bash
56
+ # Docker registry access (required)
57
+ export KAMAL_REGISTRY_PASSWORD="docker-registry-password"
58
+
59
+ # Litestream backup configuration (optional)
60
+ export LITESTREAM_ACCESS_KEY_ID="access-key"
61
+ export LITESTREAM_SECRET_ACCESS_KEY="secret-access-key"
62
+ export LITESTREAM_BUCKET="https://eu2.yourbucketendpoint.com/"
63
+ export LITESTREAM_REGION="eu2"
64
+ ```
65
+
66
+ #### B. Install and Deploy
67
+
68
+ ```bash
69
+ gem install railsmaker-core
70
+
71
+ # Interactive wizard (2 minutes)
72
+ railsmaker new:wizard
73
+
74
+ # Deploy to any cloud
75
+ kamal setup
76
+ ```
77
+
78
+ If you have chosen to include litestream keep in mind that the corresponding kamal accessory will also be deployed.
79
+
80
+ ### 2. Setting up Monitoring (Optional)
81
+
82
+ #### A. Install SigNoz Server
83
+ ```bash
84
+ railsmaker remote signoz \
85
+ --ssh-host=monitor.example.com \
86
+ --ssh-user=deploy
87
+ ```
88
+
89
+ #### B. Add OpenTelemetry Collector to apps server
90
+ ```bash
91
+ railsmaker remote signoz:opentelemetry \
92
+ --ssh-host=app.example.com \
93
+ --ssh-user=deploy \
94
+ --signoz-host=monitor.example.com \
95
+ --hostname=my-production-apps
96
+ ```
97
+
98
+ ### 3. Setting up Analytics (Optional)
99
+ ```bash
100
+ railsmaker remote plausible \
101
+ --ssh-host=analytics.example.com \
102
+ --ssh-user=deploy \
103
+ --analytics-host=plausible.example.com
104
+ ```
105
+
106
+ ### Verification
107
+
108
+ - SigNoz Dashboard: `https://monitor.example.com:3301`
109
+ - Plausible Analytics: `https://analytics.example.com`
110
+ - Your App: `https://app.example.com`
111
+
112
+ > **Note**: All services are tested on Ubuntu 24.04 and macOS 15.2.
113
+
114
+ **For a more detailed guide, check out [10 Steps To Prod](./10-STEPS-TO-PROD.md).**
115
+
116
+ ### Environment Requirements
117
+
118
+ - **SigNoz Server**: 2 CPU, 4GB RAM minimum
119
+ - **Plausible**: 1 CPU, 2GB RAM minimum
120
+ - **App Server**: 1 CPU, 2GB RAM minimum
121
+
122
+ You can decide how to split the services between your servers (e.g. SigNoz & Plausible on a separate server from the app or apps).
123
+
124
+ ### Database Recovery
125
+
126
+ In case of DB failure, follow these steps to recover your data:
127
+
128
+ 1. Stop the application:
129
+ ```bash
130
+ kamal app stop
131
+ ```
132
+
133
+ 2. Remove existing database files:
134
+ ```bash
135
+ kamal app exec /bin/sh -i
136
+ rm -rf ./storage/*
137
+ exit
138
+ ```
139
+
140
+ 3. Recover files and set proper ownership to files:
141
+ ```bash
142
+ kamal restore-db-app
143
+ kamal restore-db-cache
144
+ kamal restore-db-queue
145
+ kamal restore-db-cable
146
+ kamal restore-db-ownership
147
+ ```
148
+
149
+ 4. Restart Litestream to initiate recovery:
150
+ ```bash
151
+ kamal accessory reboot litestream
152
+ ```
153
+
154
+ 5. Start the application:
155
+ ```bash
156
+ kamal app boot
157
+ ```
158
+
159
+ ### Cloudflare DNS
160
+
161
+ If you are relying on Cloudflare, make sure you set-up SSL/TLS to Full.
162
+
163
+ ## Support
164
+
165
+ This project is **pay-what-you-want**. If it helps you ship faster:
166
+
167
+ [![Support](https://img.shields.io/badge/Support-%F0%9F%8D%B8-yellow?style=for-the-badge)](https://buymeacoffee.com/sgerov)
168
+
169
+ *Give it a try at [railsmaker.com](https://railsmaker.com)*
data/bin/railsmaker ADDED
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'railsmaker'
5
+ require 'thor'
6
+
7
+ module RailsMaker
8
+ class BaseCLI < Thor
9
+ include Thor::Actions
10
+
11
+ no_commands do
12
+ def colorize(str, color)
13
+ case color
14
+ when :green then "\e[32m#{str}\e[0m"
15
+ when :red then "\e[31m#{str}\e[0m"
16
+ when :blue then "\e[34m#{str}\e[0m"
17
+ when :gray then "\e[90m#{str}\e[0m"
18
+ else str
19
+ end
20
+ end
21
+
22
+ def status_indicator(value)
23
+ if value == true || (value.is_a?(String) && !value.empty?)
24
+ colorize('●', :green) + ' Enabled'
25
+ else
26
+ colorize('○', :red) + ' Disabled'
27
+ end
28
+ end
29
+
30
+ def print_summary(title, sections)
31
+ max_label_width = sections.values.flatten(1).map { |item| item[0].length }.max
32
+
33
+ say "\n#{colorize("═══ #{title} ═══", :blue)}\n\n"
34
+
35
+ sections.each do |section_title, items|
36
+ say colorize(section_title.upcase, :gray)
37
+ say colorize('─' * section_title.length, :gray)
38
+
39
+ items.each do |label, value, format = :default|
40
+ formatted_value = case format
41
+ when :status then status_indicator(value)
42
+ when :highlight then colorize(value, :blue)
43
+ else value.to_s
44
+ end
45
+
46
+ say " #{label.ljust(max_label_width)} #{formatted_value}"
47
+ end
48
+ say "\n"
49
+ end
50
+ end
51
+
52
+ def to_generator_args(options)
53
+ options.flat_map { |key, value| ["--#{key}", value.to_s] }
54
+ end
55
+ end
56
+ end
57
+
58
+ class RemoteCLI < BaseCLI
59
+ map 'signoz:opentelemetry' => :signoz_opentelemetry
60
+
61
+ desc 'signoz SERVER', 'Install SigNoz observability server on a remote machine'
62
+ long_desc <<-LONGDESC
63
+ Installs SigNoz server on a remote machine. SigNoz is a full-stack open-source APM#{' '}
64
+ and observability tool.
65
+
66
+ Examples:
67
+ # Install using SSH key authentication
68
+ $ railsmaker remote signoz --ssh-host=example.com --ssh-user=deploy --key-path=~/.ssh/id_rsa
69
+
70
+ # Install using password authentication (will prompt for password)
71
+ $ railsmaker remote signoz --ssh-host=example.com --ssh-user=deploy
72
+
73
+ # Force reinstallation of existing instance
74
+ $ railsmaker remote signoz --ssh-host=example.com --ssh-user=deploy --force
75
+ LONGDESC
76
+ method_option :ssh_host, type: :string, required: true, desc: 'SSH host (e.g., 123.456.789.0)'
77
+ method_option :ssh_user, type: :string, default: 'root', desc: 'SSH user with sudo access'
78
+ method_option :key_path, type: :string, desc: 'Path to SSH key (if not provided, will use password auth)'
79
+ method_option :force, type: :boolean, default: false, desc: 'Force reinstallation if already installed'
80
+ def signoz
81
+ print_remote_summary('SigNoz Installation', options)
82
+ RailsMaker::Generators::SignozGenerator.start(to_generator_args(options))
83
+ end
84
+
85
+ desc 'signoz:opentelemetry', 'Install SigNoz OpenTelemetry collector on a remote machine'
86
+ long_desc <<-LONGDESC
87
+ Installs and configures the OpenTelemetry collector on a remote machine to send metrics,#{' '}
88
+ traces, and logs to your SigNoz server.
89
+
90
+ The collector will be configured to:
91
+ • Collect Docker container logs
92
+ • Forward metrics and traces
93
+ • Auto-discover running services
94
+
95
+ Examples:
96
+ # Basic installation
97
+ $ railsmaker remote signoz:opentelemetry --ssh-host=app.example.com --ssh-user=deploy \\
98
+ --signoz-host=signoz.example.com
99
+
100
+ # With custom hostname for better identification
101
+ $ railsmaker remote signoz:opentelemetry --ssh-host=app.example.com --ssh-user=deploy \\
102
+ --signoz-host=signoz.example.com --hostname=production-app-1
103
+ LONGDESC
104
+ method_option :ssh_host, type: :string, required: true, desc: 'SSH host where collector will be installed'
105
+ method_option :signoz_server_host, type: :string, required: true, desc: 'Host where SigNoz server is running'
106
+ method_option :ssh_user, type: :string, default: 'root', desc: 'SSH user with sudo access'
107
+ method_option :hostname, type: :string, default: 'rails-apps-1', desc: 'Custom hostname identifier of the server'
108
+ method_option :key_path, type: :string, desc: 'Path to SSH key (if not provided, will use password auth)'
109
+ method_option :force, type: :boolean, default: false, desc: 'Force reinstallation if already installed'
110
+ def signoz_opentelemetry
111
+ print_remote_summary('SigNoz OpenTelemetry Installation', options)
112
+ RailsMaker::Generators::SignozOpentelemetryGenerator.start(to_generator_args(options))
113
+ end
114
+
115
+ desc 'plausible', 'Install Plausible Analytics on a remote server'
116
+ long_desc <<-LONGDESC
117
+ Installs Plausible Analytics on a remote machine. Plausible is a lightweight,#{' '}
118
+ open-source alternative to Google Analytics.
119
+
120
+ Examples:
121
+ # Basic installation
122
+ $ railsmaker remote plausible --ssh-host=analytics.example.com --ssh-user=deploy \\
123
+ --analytics-host=plausible.example.com
124
+
125
+ # With custom SSH key
126
+ $ railsmaker remote plausible --ssh-host=analytics.example.com --ssh-user=deploy \\
127
+ --analytics-host=plausible.example.com --key-path=~/.ssh/analytics_key
128
+ LONGDESC
129
+ method_option :ssh_host, type: :string, required: true, desc: 'SSH host where Plausible will be installed'
130
+ method_option :analytics_host, type: :string, required: true, desc: 'Domain where Plausible will be accessible'
131
+ method_option :ssh_user, type: :string, default: 'root', desc: 'SSH user with sudo access'
132
+ method_option :key_path, type: :string, desc: 'Path to SSH key (if not provided, will use password auth)'
133
+ method_option :force, type: :boolean, default: false, desc: 'Force reinstallation if already installed'
134
+ def plausible
135
+ print_remote_summary('Plausible Installation', options)
136
+ RailsMaker::Generators::PlausibleGenerator.start(to_generator_args(options))
137
+ end
138
+
139
+ private
140
+
141
+ def print_remote_summary(title, opts)
142
+ sections = {
143
+ 'Connection Details' => [
144
+ ['SSH Host', opts[:ssh_host], :highlight],
145
+ ['SSH User', opts[:ssh_user], :highlight]
146
+ ]
147
+ }
148
+
149
+ if opts[:signoz_server_host]
150
+ sections['Configuration'] = [
151
+ ['SigNoz Host', opts[:signoz_server_host], :highlight],
152
+ ['Custom Hostname', opts[:hostname], :highlight]
153
+ ]
154
+ end
155
+
156
+ if opts[:analytics_host]
157
+ sections['Configuration'] = [
158
+ ['Analytics Host', opts[:analytics_host], :highlight]
159
+ ]
160
+ end
161
+
162
+ print_summary(title, sections)
163
+ end
164
+ end
165
+
166
+ class CLI < BaseCLI
167
+ map 'new:wizard' => :new_wizard
168
+
169
+ desc 'new', 'Generate a new Rails application with integrated features'
170
+ method_option :name, type: :string, required: true, aliases: '-n', desc: 'Application name'
171
+ method_option :docker, type: :string, required: true, aliases: '-d', desc: 'Docker username'
172
+ method_option :ip, type: :string, required: true, aliases: '-i', desc: 'Server IP address'
173
+ method_option :domain, type: :string, required: true, aliases: '-D', desc: 'Domain name'
174
+ method_option :auth, type: :boolean, default: true, desc: 'Include authentication'
175
+ method_option :mailjet, type: :boolean, default: true, desc: 'Configure Mailjet for email'
176
+ method_option :bucketname, type: :string, desc: 'Enable litestream backups (provide your BUCKETNAME)'
177
+ method_option :opentelemetry, type: :boolean, default: true, desc: 'Configure OpenTelemetry'
178
+ method_option :analytics, type: :string, desc: 'Set up Plausible Analytics (provide your ANALYTICS_DOMAIN)'
179
+ method_option :sentry, type: :boolean, default: true, desc: 'Configure Sentry error tracking'
180
+ method_option :ui, type: :boolean, default: true, desc: 'Include UI assets'
181
+ def new
182
+ self.destination_root = File.expand_path(options[:name], Dir.pwd)
183
+ say "Generating new Rails application: #{options[:name]}", :yellow
184
+ generate_application(options)
185
+ end
186
+
187
+ desc 'new:wizard', 'Launch an interactive wizard for generating a Rails application'
188
+ def new_wizard
189
+ say 'Welcome to the RailsMaker wizard!', :blue
190
+ wizard_options = collect_wizard_options
191
+ self.destination_root = File.expand_path(wizard_options[:name], Dir.pwd)
192
+ say "Generating new Rails application: #{wizard_options[:name]}", :yellow
193
+ generate_application(wizard_options)
194
+ end
195
+
196
+ desc 'remote', 'Manage remote services'
197
+ subcommand 'remote', RailsMaker::RemoteCLI
198
+
199
+ def self.exit_on_failure?
200
+ true
201
+ end
202
+
203
+ private
204
+
205
+ def cleanup_on_failure
206
+ return unless File.directory?(destination_root)
207
+ return unless $!.to_s.include?('Failed to generate app')
208
+
209
+ say_status 'cleanup', "Removing directory: #{destination_root}", :yellow
210
+ FileUtils.rm_rf(destination_root)
211
+ end
212
+
213
+ def generate_application(opts)
214
+ print_app_summary(opts)
215
+ return unless yes?('Do you want to proceed with the installation? (y/N)')
216
+
217
+ begin
218
+ generate_components(opts)
219
+ say 'Successfully generated RailsMaker template 🎉', :green
220
+ rescue StandardError => e
221
+ cleanup_on_failure
222
+ raise
223
+ end
224
+ end
225
+
226
+ def print_app_summary(opts)
227
+ sections = {
228
+ 'Application Settings' => [
229
+ ['Name:', opts[:name], :highlight],
230
+ ['Docker:', opts[:docker], :highlight],
231
+ ['IP:', opts[:ip], :highlight],
232
+ ['Domain:', opts[:domain], :highlight]
233
+ ],
234
+ 'Features & Integrations' => [
235
+ ['Authentication', opts[:auth], :status],
236
+ ['Mailjet Email', opts[:mailjet], :status],
237
+ ['Litestream Backups', opts[:bucketname], :status],
238
+ ['OpenTelemetry', opts[:opentelemetry], :status],
239
+ ['Plausible Analytics', opts[:analytics], :status],
240
+ ['Sentry Error Tracking', opts[:sentry], :status],
241
+ ['UI Components', opts[:ui], :status]
242
+ ]
243
+ }
244
+
245
+ print_summary('Configuration Summary', sections)
246
+ print_warnings(opts)
247
+ end
248
+
249
+ def print_warnings(opts)
250
+ warnings = []
251
+ if !opts[:ui] && opts[:analytics]
252
+ warnings << "- Analytics was enabled but won't be installed because UI is disabled"
253
+ end
254
+
255
+ if !opts[:ui] && opts[:auth]
256
+ warnings << "- Authentication was enabled but won't be installed because UI is disabled"
257
+ end
258
+
259
+ return unless warnings.any?
260
+
261
+ say "\nWarnings:", :yellow
262
+ warnings.each { |warning| say warning, :yellow }
263
+ end
264
+
265
+ def generate_components(opts)
266
+ generator_args = to_generator_args(opts)
267
+
268
+ RailsMaker::Generators::AppGenerator.start(generator_args)
269
+
270
+ if opts[:ui]
271
+ RailsMaker::Generators::AuthGenerator.start(generator_args) if opts[:auth]
272
+ RailsMaker::Generators::UiGenerator.start(generator_args)
273
+ RailsMaker::Generators::PlausibleInstrumentationGenerator.start(generator_args) if opts[:analytics]
274
+ end
275
+
276
+ RailsMaker::Generators::MailjetGenerator.start(generator_args) if opts[:mailjet]
277
+ RailsMaker::Generators::LitestreamGenerator.start(generator_args) if opts[:bucketname]
278
+ RailsMaker::Generators::OpentelemetryGenerator.start(generator_args) if opts[:opentelemetry]
279
+ RailsMaker::Generators::SentryGenerator.start(generator_args) if opts[:sentry]
280
+ end
281
+
282
+ def collect_wizard_options
283
+ wizard_options = {
284
+ name: options[:name],
285
+ docker: options[:docker],
286
+ ip: options[:ip],
287
+ domain: options[:domain],
288
+ auth: options[:auth],
289
+ mailjet: options[:mailjet],
290
+ bucketname: options[:bucketname],
291
+ opentelemetry: options[:opentelemetry],
292
+ analytics: options[:analytics],
293
+ sentry: options[:sentry],
294
+ ui: options[:ui]
295
+ }
296
+
297
+ # Only ask for values that weren't provided via command line
298
+ while wizard_options[:name].to_s.empty?
299
+ wizard_options[:name] = ask('Application name:')
300
+ say 'Application name is required', :red if wizard_options[:name].empty?
301
+ end
302
+
303
+ while wizard_options[:docker].to_s.empty?
304
+ wizard_options[:docker] = ask('Docker username:')
305
+ say 'Docker username is required', :red if wizard_options[:docker].empty?
306
+ end
307
+
308
+ while wizard_options[:ip].to_s.empty?
309
+ wizard_options[:ip] = ask('Server IP address:')
310
+ say 'Server IP address is required', :red if wizard_options[:ip].empty?
311
+ end
312
+
313
+ while wizard_options[:domain].to_s.empty?
314
+ wizard_options[:domain] = ask('Domain name:')
315
+ say 'Domain name is required', :red if wizard_options[:domain].empty?
316
+ end
317
+
318
+ wizard_options[:auth] = yes?('Include authentication? (y/N)') if wizard_options[:auth].nil?
319
+ wizard_options[:mailjet] = yes?('Configure Mailjet for email? (y/N)') if wizard_options[:mailjet].nil?
320
+ wizard_options[:bucketname] ||= ask('Litestream bucketname: Provide BUCKET_NAME (leave blank to skip):')
321
+ if wizard_options[:opentelemetry].nil?
322
+ wizard_options[:opentelemetry] =
323
+ yes?('Configure OpenTelemetry for metrics? (y/N)')
324
+ end
325
+ wizard_options[:analytics] ||= ask('Plausible Analytics: Provide ANALYTICS_DOMAIN (leave blank to skip):')
326
+ wizard_options[:sentry] = yes?('Configure Sentry error tracking? (y/N)') if wizard_options[:sentry].nil?
327
+ wizard_options[:ui] = yes?('Include UI assets? (y/N)') if wizard_options[:ui].nil?
328
+
329
+ wizard_options[:bucketname] = nil if wizard_options[:bucketname] && wizard_options[:bucketname].empty?
330
+ wizard_options[:analytics] = nil if wizard_options[:analytics] && wizard_options[:analytics].empty?
331
+
332
+ wizard_options
333
+ end
334
+ end
335
+ end
336
+
337
+ begin
338
+ RailsMaker::CLI.start
339
+ rescue StandardError => e
340
+ puts "\nError: #{e.message}"
341
+ puts e.backtrace if ENV['DEBUG']
342
+ puts "\nIf you need help, please create an issue at: https://github.com/sgerov/railsmaker/issues"
343
+ exit 1
344
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsMaker
4
+ module Generators
5
+ class AppGenerator < BaseGenerator
6
+ source_root File.expand_path('templates/app', __dir__)
7
+
8
+ class_option :name, type: :string, required: true, desc: 'Name of the application'
9
+ class_option :docker, type: :string, required: true, desc: 'Docker username'
10
+ class_option :ip, type: :string, required: true, desc: 'Server IP address'
11
+ class_option :domain, type: :string, required: true, desc: 'Domain name'
12
+ class_option :ui, type: :boolean, default: false, desc: 'Include UI assets?'
13
+
14
+ def check_required_env_vars
15
+ super(%w[
16
+ KAMAL_REGISTRY_PASSWORD
17
+ ])
18
+ end
19
+
20
+ def generate_app
21
+ self.destination_root = File.expand_path(options[:name], current_dir)
22
+
23
+ if !in_minitest? && File.directory?(destination_root)
24
+ say_status 'error', "Directory '#{options[:name]}' already exists", :red
25
+ raise BaseGeneratorError, 'Directory already exists'
26
+ end
27
+
28
+ say('Creating new Rails app')
29
+ rails_args = [options[:name]]
30
+ rails_args << '--javascript=bun' if options[:ui]
31
+ Rails::Generators::AppGenerator.start(rails_args)
32
+
33
+ setup_frontend if options[:ui]
34
+
35
+ validate_gsub_strings([
36
+ {
37
+ file: 'config/deploy.yml',
38
+ patterns: ['your-user', "web:\n - 192.168.0.1", 'ssl: true', 'app.example.com']
39
+ }
40
+ ])
41
+
42
+ setup_kamal
43
+
44
+ say('Modifying ApplicationController to allow all browsers (mobile)')
45
+ comment_lines 'app/controllers/application_controller.rb', /allow_browser versions: :modern/
46
+
47
+ say('Generating main controller with a landing page')
48
+ generate :controller, 'main'
49
+
50
+ if options[:ui]
51
+ copy_file 'main_index.html.erb', 'app/views/main/index.html.erb'
52
+ else
53
+ create_file 'app/views/main/index.html.erb', "<h1>Welcome to #{options[:name]}</h1>"
54
+ end
55
+
56
+ copy_file 'credentials.example.yml', 'config/credentials.example.yml'
57
+
58
+ route "root 'main#index'"
59
+ rescue StandardError => e
60
+ say_status 'error', "Failed to generate app: #{e.message} in #{destination_root}", :red
61
+ raise BaseGeneratorError, e.message
62
+ end
63
+
64
+ def git_commit
65
+ git add: '.', commit: %(-m 'Initial railsmaker commit')
66
+
67
+ say 'Successfully created Rails app with RailsMaker', :green
68
+ end
69
+
70
+ private
71
+
72
+ def setup_frontend
73
+ say('Adding Tailwind CSS')
74
+ gem 'tailwindcss-rails', '~> 4.0.0.rc5'
75
+
76
+ say('Installing gems')
77
+ run 'bundle install --quiet'
78
+
79
+ say('Setting up Tailwind')
80
+ run 'bun add -d tailwindcss@4.0.0 @tailwindcss/cli@4.0.0'
81
+ rails_command 'tailwindcss:install'
82
+
83
+ say('Installing DaisyUI')
84
+ run 'bun add -d daisyui@5.0.0-beta.2'
85
+
86
+ validate_gsub_strings([
87
+ {
88
+ file: 'app/views/layouts/application.html.erb',
89
+ patterns: ['<main class="container mx-auto mt-28 px-5 flex">', '<html>']
90
+ },
91
+ {
92
+ file: 'app/assets/tailwind/application.css',
93
+ patterns: ['@import "tailwindcss";']
94
+ },
95
+ {
96
+ file: 'Dockerfile',
97
+ patterns: ['bun.lockb', 'RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile',
98
+ '# Precompiling assets for production']
99
+ },
100
+ {
101
+ file: 'app/views/layouts/application.html.erb',
102
+ patterns: []
103
+ }
104
+ ])
105
+
106
+ inject_into_file 'app/assets/tailwind/application.css', after: '@import "tailwindcss";' do
107
+ <<~RUBY
108
+
109
+
110
+ @plugin "daisyui" {
111
+ themes: light --default, dark --prefersdark, cupcake;
112
+ }
113
+ RUBY
114
+ end
115
+ gsub_file 'app/views/layouts/application.html.erb', '<html>', '<html data-theme="cupcake">'
116
+ gsub_file 'app/views/layouts/application.html.erb', '<main class="container mx-auto mt-28 px-5 flex">',
117
+ '<main>'
118
+
119
+ say('Dockerfile: fixing legacy bun.lockb')
120
+ gsub_file 'Dockerfile', 'bun.lockb', 'bun.lock'
121
+
122
+ say('Dockerfile: fixing for Apple Silicon amd64 emulation')
123
+ gsub_file 'Dockerfile',
124
+ 'RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile',
125
+ 'RUN TAILWINDCSS_INSTALL_DIR=node_modules/.bin SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile'
126
+ inject_into_file 'Dockerfile', before: /# Precompiling assets for production/ do
127
+ <<~RUBY
128
+ # Ensure bun handles tailwind node bash binary
129
+ RUN ln -s /usr/local/bun/bin/bun /usr/local/bun/bin/node\n
130
+ RUBY
131
+ end
132
+ end
133
+
134
+ def setup_kamal
135
+ say('Configuring Kamal')
136
+
137
+ inject_into_file 'config/deploy.yml', after: 'service: railsmaker' do
138
+ "\ndeploy_timeout: 60 # to avoid timeout on first deploy"
139
+ end
140
+ gsub_file 'config/deploy.yml', 'your-user', options[:docker]
141
+ gsub_file 'config/deploy.yml', "web:\n - 192.168.0.1", "web:\n hosts:\n - #{options[:ip]}"
142
+ gsub_file 'config/deploy.yml', 'app.example.com', options[:domain]
143
+ inject_into_file 'config/deploy.yml', after: 'ssl: true' do
144
+ "\n forward_headers: true"
145
+ end
146
+ end
147
+
148
+ def current_dir
149
+ Dir.pwd
150
+ end
151
+
152
+ def in_minitest?
153
+ defined?(Minitest)
154
+ end
155
+ end
156
+ end
157
+ end