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.
- checksums.yaml +7 -0
- data/README.md +169 -0
- data/bin/railsmaker +344 -0
- data/lib/railsmaker/generators/app_generator.rb +157 -0
- data/lib/railsmaker/generators/auth_generator.rb +97 -0
- data/lib/railsmaker/generators/base_generator.rb +39 -0
- data/lib/railsmaker/generators/concerns/gsub_validation.rb +24 -0
- data/lib/railsmaker/generators/litestream_generator.rb +89 -0
- data/lib/railsmaker/generators/mailjet_generator.rb +68 -0
- data/lib/railsmaker/generators/opentelemetry_generator.rb +79 -0
- data/lib/railsmaker/generators/plausible_generator.rb +33 -0
- data/lib/railsmaker/generators/plausible_instrumentation_generator.rb +28 -0
- data/lib/railsmaker/generators/sentry_generator.rb +46 -0
- data/lib/railsmaker/generators/server_command_generator.rb +178 -0
- data/lib/railsmaker/generators/signoz_generator.rb +33 -0
- data/lib/railsmaker/generators/signoz_opentelemetry_generator.rb +37 -0
- data/lib/railsmaker/generators/templates/app/credentials.example.yml +14 -0
- data/lib/railsmaker/generators/templates/app/main_index.html.erb +71 -0
- data/lib/railsmaker/generators/templates/auth/app/controllers/omniauth_callbacks_controller.rb +18 -0
- data/lib/railsmaker/generators/templates/litestream/litestream.yml.erb +32 -0
- data/lib/railsmaker/generators/templates/opentelemetry/lograge.rb.erb +9 -0
- data/lib/railsmaker/generators/templates/shell_scripts/plausible.sh.erb +51 -0
- data/lib/railsmaker/generators/templates/shell_scripts/signoz.sh.erb +38 -0
- data/lib/railsmaker/generators/templates/shell_scripts/signoz_opentelemetry.sh.erb +49 -0
- data/lib/railsmaker/generators/templates/ui/app/assets/images/og-image.webp +0 -0
- data/lib/railsmaker/generators/templates/ui/app/assets/images/plausible-screenshot.png +0 -0
- data/lib/railsmaker/generators/templates/ui/app/assets/images/signoz-screenshot.png +0 -0
- data/lib/railsmaker/generators/templates/ui/app/controllers/demo_controller.rb +7 -0
- data/lib/railsmaker/generators/templates/ui/app/controllers/pages_controller.rb +4 -0
- data/lib/railsmaker/generators/templates/ui/app/helpers/seo_helper.rb +39 -0
- data/lib/railsmaker/generators/templates/ui/app/javascript/controllers/flash_controller.js +14 -0
- data/lib/railsmaker/generators/templates/ui/app/javascript/controllers/index.js +11 -0
- data/lib/railsmaker/generators/templates/ui/app/javascript/controllers/scroll_fade_controller.js +27 -0
- data/lib/railsmaker/generators/templates/ui/app/views/clearance_mailer/change_password.html.erb +8 -0
- data/lib/railsmaker/generators/templates/ui/app/views/clearance_mailer/change_password.text.erb +5 -0
- data/lib/railsmaker/generators/templates/ui/app/views/demo/analytics.html.erb +214 -0
- data/lib/railsmaker/generators/templates/ui/app/views/demo/index.html.erb +312 -0
- data/lib/railsmaker/generators/templates/ui/app/views/demo/support.html.erb +147 -0
- data/lib/railsmaker/generators/templates/ui/app/views/layouts/_navbar.html.erb +193 -0
- data/lib/railsmaker/generators/templates/ui/app/views/layouts/application.html.erb +62 -0
- data/lib/railsmaker/generators/templates/ui/app/views/main/index.html.erb +320 -0
- data/lib/railsmaker/generators/templates/ui/app/views/pages/privacy.html.erb +63 -0
- data/lib/railsmaker/generators/templates/ui/app/views/pages/terms.html.erb +54 -0
- data/lib/railsmaker/generators/templates/ui/app/views/passwords/create.html.erb +9 -0
- data/lib/railsmaker/generators/templates/ui/app/views/passwords/edit.html.erb +21 -0
- data/lib/railsmaker/generators/templates/ui/app/views/passwords/new.html.erb +26 -0
- data/lib/railsmaker/generators/templates/ui/app/views/sessions/new.html.erb +49 -0
- data/lib/railsmaker/generators/templates/ui/app/views/shared/_auth_layout.html.erb +24 -0
- data/lib/railsmaker/generators/templates/ui/app/views/shared/_flash.html.erb +19 -0
- data/lib/railsmaker/generators/templates/ui/app/views/shared/_footer.html.erb +52 -0
- data/lib/railsmaker/generators/templates/ui/app/views/shared/_structured_data.html.erb +20 -0
- data/lib/railsmaker/generators/templates/ui/app/views/users/new.html.erb +49 -0
- data/lib/railsmaker/generators/templates/ui/config/sitemap.rb +33 -0
- data/lib/railsmaker/generators/templates/ui/public/icon.png +0 -0
- data/lib/railsmaker/generators/templates/ui/public/icon.svg +5 -0
- data/lib/railsmaker/generators/templates/ui/public/robots.txt +8 -0
- data/lib/railsmaker/generators/ui_generator.rb +68 -0
- data/lib/railsmaker.rb +29 -0
- metadata +359 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsMaker
|
4
|
+
module Generators
|
5
|
+
class AuthGenerator < BaseGenerator
|
6
|
+
source_root File.expand_path('templates/auth', __dir__)
|
7
|
+
|
8
|
+
def add_gems
|
9
|
+
gem_group :default do
|
10
|
+
gem 'clearance', '~> 2.9.3'
|
11
|
+
gem 'omniauth', '~> 2.1.2'
|
12
|
+
gem 'omniauth-google-oauth2', '~> 1.2.1'
|
13
|
+
gem 'omniauth-rails_csrf_protection', '~> 1.0.2'
|
14
|
+
end
|
15
|
+
|
16
|
+
run 'bundle install'
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup_clearance
|
20
|
+
generate 'clearance:install'
|
21
|
+
rake 'db:migrate'
|
22
|
+
generate 'clearance:views'
|
23
|
+
end
|
24
|
+
|
25
|
+
def configure_clearance
|
26
|
+
gsub_file 'config/initializers/clearance.rb',
|
27
|
+
'config.mailer_sender = "reply@example.com"',
|
28
|
+
'config.mailer_sender = Rails.application.credentials.dig(:app, :mailer_sender)'
|
29
|
+
|
30
|
+
inject_into_file 'config/initializers/clearance.rb', after: 'Clearance.configure do |config|' do
|
31
|
+
"\n config.redirect_url = \"/demo\""
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def setup_omniauth
|
36
|
+
create_file 'config/initializers/omniauth.rb' do
|
37
|
+
<<~RUBY
|
38
|
+
# frozen_string_literal: true
|
39
|
+
|
40
|
+
Rails.application.config.middleware.use OmniAuth::Builder do
|
41
|
+
provider :google_oauth2,
|
42
|
+
Rails.application.credentials.dig(:google_oauth, :client_id),
|
43
|
+
Rails.application.credentials.dig(:google_oauth, :client_secret),
|
44
|
+
{
|
45
|
+
scope: "email",
|
46
|
+
prompt: "select_account"
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
OmniAuth.config.allowed_request_methods = %i[get]
|
51
|
+
RUBY
|
52
|
+
end
|
53
|
+
|
54
|
+
generate 'migration', 'AddOmniauthToUsers provider:string uid:string'
|
55
|
+
inject_into_file Dir['db/migrate/*add_omniauth_to_users.rb'].first,
|
56
|
+
after: "add_column :users, :uid, :string\n" do
|
57
|
+
<<-RUBY
|
58
|
+
add_index :users, [:provider, :uid], unique: true
|
59
|
+
RUBY
|
60
|
+
end
|
61
|
+
|
62
|
+
inject_into_file 'app/models/user.rb', after: "include Clearance::User\n" do
|
63
|
+
<<-RUBY
|
64
|
+
|
65
|
+
def self.from_omniauth(auth)
|
66
|
+
where(provider: auth.provider, uid: auth.uid).first_or_create do |user|
|
67
|
+
user.email = auth.info.email
|
68
|
+
user.password = SecureRandom.hex(10)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
RUBY
|
72
|
+
end
|
73
|
+
|
74
|
+
rake 'db:migrate'
|
75
|
+
end
|
76
|
+
|
77
|
+
def add_omniauth_controller
|
78
|
+
template 'app/controllers/omniauth_callbacks_controller.rb',
|
79
|
+
'app/controllers/omniauth_callbacks_controller.rb'
|
80
|
+
end
|
81
|
+
|
82
|
+
def update_routes
|
83
|
+
route <<~RUBY
|
84
|
+
# OmniAuth callback routes
|
85
|
+
get "auth/:provider/callback", to: "omniauth_callbacks#google_oauth2", constraints: { provider: "google_oauth2" }
|
86
|
+
get 'auth/failure', to: 'omniauth_callbacks#failure'
|
87
|
+
RUBY
|
88
|
+
end
|
89
|
+
|
90
|
+
def git_commit
|
91
|
+
git add: '.', commit: %(-m 'Add authentication with Clearance and OmniAuth')
|
92
|
+
|
93
|
+
say 'Successfully added authentication with Clearance and OmniAuth', :green
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsMaker
|
4
|
+
module Generators
|
5
|
+
class BaseGenerator < Rails::Generators::Base
|
6
|
+
class BaseGeneratorError < StandardError; end
|
7
|
+
|
8
|
+
include Rails::Generators::Actions
|
9
|
+
include GsubValidation
|
10
|
+
|
11
|
+
def self.default_command_options
|
12
|
+
{ abort_on_failure: true }
|
13
|
+
end
|
14
|
+
|
15
|
+
protected
|
16
|
+
|
17
|
+
def run(*args)
|
18
|
+
options = args.extract_options!
|
19
|
+
options = self.class.default_command_options.merge(options)
|
20
|
+
super(*args, options.merge(force: true)) # do not ask for confirmation on overrides
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def check_required_env_vars(required_vars)
|
26
|
+
missing_vars = required_vars.reject { |var| ENV[var].present? }
|
27
|
+
|
28
|
+
return if missing_vars.empty?
|
29
|
+
|
30
|
+
say "\nError: Missing required environment variables:", :red
|
31
|
+
missing_vars.each { |var| say " - #{var}", :red }
|
32
|
+
say "\nPlease set these environment variables before continuing:", :red
|
33
|
+
missing_vars.each { |var| say " export #{var}=your-value", :yellow }
|
34
|
+
|
35
|
+
raise BaseGeneratorError, 'Missing required environment variables'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsMaker
|
4
|
+
module Generators
|
5
|
+
module GsubValidation
|
6
|
+
def validate_gsub_strings(validations)
|
7
|
+
validations.each do |validation|
|
8
|
+
file_path = File.join(destination_root, validation.fetch(:file))
|
9
|
+
|
10
|
+
unless File.exist?(file_path)
|
11
|
+
raise "Required file not found: #{validation.fetch(:file)}. Maybe a dependency changed?"
|
12
|
+
end
|
13
|
+
|
14
|
+
content = File.read(file_path)
|
15
|
+
validation.fetch(:patterns).each do |pattern|
|
16
|
+
unless content.include?(pattern)
|
17
|
+
raise "Expected to find '#{pattern}' in #{validation.fetch(:file)} but didn't. Maybe a dependency changed?"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsMaker
|
4
|
+
module Generators
|
5
|
+
class LitestreamGenerator < BaseGenerator
|
6
|
+
source_root File.expand_path('templates/litestream', __dir__)
|
7
|
+
|
8
|
+
class_option :bucketname, type: :string, required: true, desc: 'Litestream bucketname'
|
9
|
+
class_option :name, type: :string, required: true, desc: 'Application name for volume and bucket naming'
|
10
|
+
class_option :ip, type: :string, required: true, desc: 'Server IP address'
|
11
|
+
|
12
|
+
def check_required_env_vars
|
13
|
+
super(%w[
|
14
|
+
LITESTREAM_ACCESS_KEY_ID
|
15
|
+
LITESTREAM_SECRET_ACCESS_KEY
|
16
|
+
LITESTREAM_ENDPOINT
|
17
|
+
LITESTREAM_REGION
|
18
|
+
])
|
19
|
+
end
|
20
|
+
|
21
|
+
def create_litestream_config
|
22
|
+
template 'litestream.yml.erb', 'config/litestream.yml'
|
23
|
+
end
|
24
|
+
|
25
|
+
def add_kamal_secrets
|
26
|
+
inject_into_file '.kamal/secrets', after: "RAILS_MASTER_KEY=$(cat config/master.key)\n" do
|
27
|
+
<<~YAML
|
28
|
+
|
29
|
+
# Litestream credentials for S3-compatible storage
|
30
|
+
LITESTREAM_ACCESS_KEY_ID=$LITESTREAM_ACCESS_KEY_ID
|
31
|
+
LITESTREAM_SECRET_ACCESS_KEY=$LITESTREAM_SECRET_ACCESS_KEY
|
32
|
+
LITESTREAM_ENDPOINT=$LITESTREAM_ENDPOINT
|
33
|
+
LITESTREAM_REGION=$LITESTREAM_REGION
|
34
|
+
LITESTREAM_BUCKET_NAME=#{options[:bucketname]}
|
35
|
+
YAML
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_to_deployment
|
40
|
+
validations = [
|
41
|
+
{
|
42
|
+
file: 'config/deploy.yml',
|
43
|
+
patterns: ["bin/rails dbconsole\"\n", "arch: amd64\n"]
|
44
|
+
}
|
45
|
+
]
|
46
|
+
|
47
|
+
validate_gsub_strings(validations)
|
48
|
+
|
49
|
+
inject_into_file 'config/deploy.yml', after: "arch: amd64\n" do
|
50
|
+
<<~YAML
|
51
|
+
|
52
|
+
accessories:
|
53
|
+
litestream:
|
54
|
+
image: litestream/litestream:0.3
|
55
|
+
host: #{options[:ip]}
|
56
|
+
volumes:
|
57
|
+
- "#{options[:name].underscore}_storage:/rails/storage"
|
58
|
+
files:
|
59
|
+
- config/litestream.yml:/etc/litestream.yml
|
60
|
+
cmd: replicate -config /etc/litestream.yml
|
61
|
+
env:
|
62
|
+
secret:
|
63
|
+
- LITESTREAM_ACCESS_KEY_ID
|
64
|
+
- LITESTREAM_SECRET_ACCESS_KEY
|
65
|
+
- LITESTREAM_ENDPOINT
|
66
|
+
- LITESTREAM_REGION
|
67
|
+
- LITESTREAM_BUCKET_NAME
|
68
|
+
YAML
|
69
|
+
end
|
70
|
+
|
71
|
+
inject_into_file 'config/deploy.yml', after: "bin/rails dbconsole\"\n" do
|
72
|
+
<<-YAML
|
73
|
+
restore-db-app: accessory exec litestream "restore -if-replica-exists -config /etc/litestream.yml /rails/storage/production.sqlite3"
|
74
|
+
restore-db-cache: accessory exec litestream "restore -if-replica-exists -config /etc/litestream.yml /rails/storage/production_cache.sqlite3"
|
75
|
+
restore-db-queue: accessory exec litestream "restore -if-replica-exists -config /etc/litestream.yml /rails/storage/production_queue.sqlite3"
|
76
|
+
restore-db-cable: accessory exec litestream "restore -if-replica-exists -config /etc/litestream.yml /rails/storage/production_cable.sqlite3"
|
77
|
+
restore-db-ownership: server exec "sudo chown -R 1000:1000 /var/lib/docker/volumes/#{options[:name].underscore}_storage/_data/"
|
78
|
+
YAML
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def git_commit
|
83
|
+
git add: '.', commit: %(-m 'Add Litestream configuration')
|
84
|
+
|
85
|
+
say 'Successfully added Litestream configuration', :green
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsMaker
|
4
|
+
module Generators
|
5
|
+
class MailjetGenerator < BaseGenerator
|
6
|
+
source_root File.expand_path('templates/mailjet', __dir__)
|
7
|
+
|
8
|
+
class_option :name, type: :string, required: true, desc: 'Name of the service for email sender'
|
9
|
+
class_option :domain, type: :string, required: true, desc: 'Host domain for the application'
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
super
|
13
|
+
@name = options[:name]
|
14
|
+
@domain = options[:domain]
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_gem
|
18
|
+
gem_group :default do
|
19
|
+
gem 'mailjet', '~> 1.8'
|
20
|
+
end
|
21
|
+
|
22
|
+
run 'bundle install'
|
23
|
+
end
|
24
|
+
|
25
|
+
def create_initializer
|
26
|
+
create_file 'config/initializers/mailjet.rb' do
|
27
|
+
<<~RUBY
|
28
|
+
# frozen_string_literal: true
|
29
|
+
|
30
|
+
Mailjet.configure do |config|
|
31
|
+
config.api_key = Rails.application.credentials.dig(:mailjet, :api_key)
|
32
|
+
config.secret_key = Rails.application.credentials.dig(:mailjet, :secret_key)
|
33
|
+
config.default_from = Rails.application.credentials.dig(:app, :mailer_sender)
|
34
|
+
config.api_version = "v3.1"
|
35
|
+
end
|
36
|
+
RUBY
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def configure_mailer
|
41
|
+
environment(nil, env: 'production') do
|
42
|
+
<<~RUBY
|
43
|
+
# Mailjet API configuration
|
44
|
+
config.action_mailer.delivery_method = :mailjet_api
|
45
|
+
RUBY
|
46
|
+
end
|
47
|
+
|
48
|
+
gsub_file 'app/mailers/application_mailer.rb',
|
49
|
+
/default from: .+$/,
|
50
|
+
'default from: Rails.application.credentials.dig(:app, :mailer_sender)'
|
51
|
+
|
52
|
+
gsub_file 'config/environments/production.rb',
|
53
|
+
/config\.action_mailer\.default_url_options = \{ host: .+\}/,
|
54
|
+
'config.action_mailer.default_url_options = { host: Rails.application.credentials.dig(:app, :host) }'
|
55
|
+
end
|
56
|
+
|
57
|
+
def git_commit
|
58
|
+
git add: '.', commit: %(-m 'Add Mailjet configuration')
|
59
|
+
|
60
|
+
say 'Successfully added Mailjet configuration', :green
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
attr_reader :name, :domain
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsMaker
|
4
|
+
module Generators
|
5
|
+
class OpentelemetryGenerator < BaseGenerator
|
6
|
+
source_root File.expand_path('templates/opentelemetry', __dir__)
|
7
|
+
|
8
|
+
class_option :name, type: :string, required: true, desc: 'Name of the application'
|
9
|
+
|
10
|
+
def add_kamal_config
|
11
|
+
validations = [
|
12
|
+
{
|
13
|
+
file: 'config/deploy.yml',
|
14
|
+
patterns: [
|
15
|
+
"web:\n",
|
16
|
+
"SOLID_QUEUE_IN_PUMA: true\n"
|
17
|
+
]
|
18
|
+
}
|
19
|
+
]
|
20
|
+
|
21
|
+
validate_gsub_strings(validations)
|
22
|
+
|
23
|
+
inject_into_file 'config/deploy.yml', after: "web:\n" do
|
24
|
+
<<-YAML
|
25
|
+
options:
|
26
|
+
"add-host": host.docker.internal:host-gateway
|
27
|
+
YAML
|
28
|
+
end
|
29
|
+
|
30
|
+
inject_into_file 'config/deploy.yml', after: "SOLID_QUEUE_IN_PUMA: true\n" do
|
31
|
+
<<-YAML
|
32
|
+
# OpenTelemetry env vars
|
33
|
+
OTEL_EXPORTER: otlp
|
34
|
+
OTEL_SERVICE_NAME: #{options[:name]}
|
35
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: http://host.docker.internal:4318
|
36
|
+
YAML
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def add_gems
|
41
|
+
gem_group :default do
|
42
|
+
gem 'opentelemetry-sdk', '~> 1.6.0'
|
43
|
+
gem 'opentelemetry-exporter-otlp', '~> 0.29.1'
|
44
|
+
gem 'opentelemetry-instrumentation-all', '~> 0.72.0'
|
45
|
+
|
46
|
+
gem 'lograge', '~> 0.14.0'
|
47
|
+
gem 'logstash-event', '~> 1.2.02'
|
48
|
+
end
|
49
|
+
|
50
|
+
run 'bundle install'
|
51
|
+
end
|
52
|
+
|
53
|
+
def configure_opentelemetry
|
54
|
+
environment_file = 'config/environment.rb'
|
55
|
+
|
56
|
+
prepend_to_file environment_file, "require 'opentelemetry/sdk'\n"
|
57
|
+
inject_into_file environment_file, before: 'Rails.application.initialize!' do
|
58
|
+
<<~RUBY
|
59
|
+
|
60
|
+
OpenTelemetry::SDK.configure do |c|
|
61
|
+
c.use_all unless Rails.env.development? || Rails.env.test?
|
62
|
+
end
|
63
|
+
|
64
|
+
RUBY
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def setup_lograge
|
69
|
+
template 'lograge.rb.erb', 'config/initializers/lograge.rb'
|
70
|
+
end
|
71
|
+
|
72
|
+
def git_commit
|
73
|
+
git add: '.', commit: %(-m 'Add OpenTelemetry')
|
74
|
+
|
75
|
+
say 'Successfully added OpenTelemetry configuration', :green
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsMaker
|
4
|
+
module Generators
|
5
|
+
class PlausibleGenerator < ServerCommandGenerator
|
6
|
+
source_root File.expand_path('templates/shell_scripts', __dir__)
|
7
|
+
|
8
|
+
class_option :analytics_host, type: :string, required: true,
|
9
|
+
desc: 'Domain where Plausible Analytics will be hosted'
|
10
|
+
|
11
|
+
def initialize(*args)
|
12
|
+
super
|
13
|
+
@analytics_host = options[:analytics_host]
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def script_name
|
19
|
+
'plausible'
|
20
|
+
end
|
21
|
+
|
22
|
+
def check_path
|
23
|
+
'~/plausible-ce'
|
24
|
+
end
|
25
|
+
|
26
|
+
def title
|
27
|
+
"Installing Plausible Analytics on remote server #{options[:ssh_user]}@#{options[:ssh_host]}"
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :analytics_host
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsMaker
|
4
|
+
module Generators
|
5
|
+
class PlausibleInstrumentationGenerator < BaseGenerator
|
6
|
+
class_option :domain, type: :string, required: true, desc: 'Domain of your application'
|
7
|
+
class_option :analytics, type: :string, required: true, desc: 'Domain where Plausible is hosted'
|
8
|
+
|
9
|
+
def add_plausible_script
|
10
|
+
content = <<~HTML.indent(4)
|
11
|
+
<%# Plausible Analytics %>
|
12
|
+
<script defer data-domain="#{options[:domain]}" src="https://#{options[:analytics]}/js/script.file-downloads.outbound-links.pageview-props.revenue.tagged-events.js"></script>
|
13
|
+
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
|
14
|
+
HTML
|
15
|
+
|
16
|
+
gsub_file 'app/views/layouts/application.html.erb',
|
17
|
+
%r{<%# Plausible Analytics %>.*?</script>\s*<script>.*?</script>}m,
|
18
|
+
content.strip.to_s
|
19
|
+
end
|
20
|
+
|
21
|
+
def git_commit
|
22
|
+
git add: '.', commit: %(-m 'Add Plausible Analytics')
|
23
|
+
|
24
|
+
say 'Successfully added Plausible Analytics', :green
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RailsMaker
|
4
|
+
module Generators
|
5
|
+
class SentryGenerator < BaseGenerator
|
6
|
+
def add_gems
|
7
|
+
gem_group :default do
|
8
|
+
gem 'sentry-ruby', '~> 5.22.3'
|
9
|
+
gem 'sentry-rails', '~> 5.22.3'
|
10
|
+
end
|
11
|
+
|
12
|
+
run 'bundle install'
|
13
|
+
end
|
14
|
+
|
15
|
+
def generate_sentry_initializer
|
16
|
+
generate 'sentry'
|
17
|
+
|
18
|
+
validations = [
|
19
|
+
{
|
20
|
+
file: 'config/initializers/sentry.rb',
|
21
|
+
patterns: [
|
22
|
+
'Sentry.init'
|
23
|
+
]
|
24
|
+
}
|
25
|
+
]
|
26
|
+
|
27
|
+
validate_gsub_strings(validations)
|
28
|
+
end
|
29
|
+
|
30
|
+
def configure_sentry
|
31
|
+
gsub_file 'config/initializers/sentry.rb', /Sentry\.init.*end\n/m do
|
32
|
+
<<~RUBY
|
33
|
+
Sentry.init do |config|
|
34
|
+
config.dsn = Rails.application.credentials.dig(:sentry_dsn)
|
35
|
+
config.breadcrumbs_logger = [ :active_support_logger, :http_logger ]
|
36
|
+
end
|
37
|
+
RUBY
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def git_commit
|
42
|
+
git add: '.', commit: %(-m 'Add Sentry')
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'English'
|
4
|
+
require 'fileutils'
|
5
|
+
require 'base64'
|
6
|
+
require 'securerandom'
|
7
|
+
|
8
|
+
module RailsMaker
|
9
|
+
module Generators
|
10
|
+
class ServerCommandGenerator < BaseGenerator
|
11
|
+
source_root File.expand_path('templates/shell_scripts', __dir__)
|
12
|
+
|
13
|
+
class_option :ssh_host, type: :string, required: true, desc: 'SSH host'
|
14
|
+
class_option :ssh_user, type: :string, required: true, desc: 'SSH user'
|
15
|
+
class_option :key_path, type: :string, desc: 'Path to SSH private key (optional)'
|
16
|
+
class_option :force, type: :boolean, default: false,
|
17
|
+
desc: 'Force installation even if already installed'
|
18
|
+
|
19
|
+
def execute_script
|
20
|
+
return unless ssh_available?
|
21
|
+
|
22
|
+
say "⚠️ WARNING: This command will SSH into #{options[:ssh_user]}@#{options[:ssh_host]} and install software.",
|
23
|
+
:yellow
|
24
|
+
return unless yes?('Do you want to proceed? (y/N)')
|
25
|
+
|
26
|
+
# Generate random suffix for tmp files
|
27
|
+
random_suffix = SecureRandom.hex(8)
|
28
|
+
|
29
|
+
remote_files_content = remote_files.map do |file|
|
30
|
+
tmp_filename = "#{file[:filename]}.#{random_suffix}"
|
31
|
+
template file[:template], "/tmp/#{tmp_filename}"
|
32
|
+
content = File.read("/tmp/#{tmp_filename}")
|
33
|
+
FileUtils.rm("/tmp/#{tmp_filename}")
|
34
|
+
[tmp_filename, Base64.strict_encode64(content)]
|
35
|
+
end.to_h
|
36
|
+
|
37
|
+
file_commands = remote_files_content.map do |filename, content|
|
38
|
+
[
|
39
|
+
"echo '#{content}' | base64 -d > /tmp/#{filename}",
|
40
|
+
filename.end_with?("install_script.sh.#{random_suffix}") ? "chmod +x /tmp/#{filename}" : nil
|
41
|
+
]
|
42
|
+
end.flatten.compact
|
43
|
+
|
44
|
+
execute_remote_commands(
|
45
|
+
[
|
46
|
+
*file_commands,
|
47
|
+
"/tmp/install_script.sh.#{random_suffix}",
|
48
|
+
*remote_files_content.keys.map { |filename| "rm /tmp/#{filename}" }
|
49
|
+
],
|
50
|
+
title: title,
|
51
|
+
check_path: check_path,
|
52
|
+
force: options[:force]
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
def script_name
|
59
|
+
raise NotImplementedError, 'Subclasses must implement #script_name'
|
60
|
+
end
|
61
|
+
|
62
|
+
def check_path
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
def title
|
67
|
+
"Executing #{script_name} script"
|
68
|
+
end
|
69
|
+
|
70
|
+
def config_files
|
71
|
+
[]
|
72
|
+
end
|
73
|
+
|
74
|
+
def remote_files
|
75
|
+
[
|
76
|
+
*config_files,
|
77
|
+
{
|
78
|
+
template: "#{script_name}.sh.erb",
|
79
|
+
filename: 'install_script.sh'
|
80
|
+
}
|
81
|
+
]
|
82
|
+
end
|
83
|
+
|
84
|
+
def install_commands
|
85
|
+
[
|
86
|
+
'/tmp/install_script.sh',
|
87
|
+
*remote_files.map { |file| "rm /tmp/#{file[:filename]}" }
|
88
|
+
]
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def ssh_available?
|
94
|
+
return true if system('which ssh', out: File::NULL)
|
95
|
+
|
96
|
+
say_status 'error', 'SSH client not found. Please install SSH first.', :red
|
97
|
+
false
|
98
|
+
end
|
99
|
+
|
100
|
+
def ssh_destination
|
101
|
+
"#{options[:ssh_user]}@#{options[:ssh_host]}"
|
102
|
+
end
|
103
|
+
|
104
|
+
def ssh_options
|
105
|
+
opts = [
|
106
|
+
'StrictHostKeyChecking=accept-new',
|
107
|
+
'ConnectTimeout=10'
|
108
|
+
]
|
109
|
+
opts.map { |opt| "-o #{opt}" }.join(' ')
|
110
|
+
end
|
111
|
+
|
112
|
+
def installation_exists?(check_path)
|
113
|
+
return false unless check_path
|
114
|
+
|
115
|
+
"[ -d #{check_path} ]"
|
116
|
+
end
|
117
|
+
|
118
|
+
def execute_remote_commands(commands, options = {})
|
119
|
+
if options[:key_path] && !File.exist?(File.expand_path(options[:key_path]))
|
120
|
+
say_status 'error', "SSH key not found: #{options[:key_path]}", :red
|
121
|
+
raise BaseGeneratorError, 'SSH key not found'
|
122
|
+
end
|
123
|
+
|
124
|
+
title = options[:title] || 'Executing remote commands'
|
125
|
+
say_status 'start', title, :blue
|
126
|
+
|
127
|
+
script_content = []
|
128
|
+
|
129
|
+
if options[:check_path] && !options[:force]
|
130
|
+
script_content << <<~SHELL
|
131
|
+
if #{installation_exists?(options[:check_path])}; then
|
132
|
+
echo "Installation already exists at #{options[:check_path]}"
|
133
|
+
echo "Use --force to reinstall"
|
134
|
+
exit 0
|
135
|
+
fi
|
136
|
+
SHELL
|
137
|
+
end
|
138
|
+
|
139
|
+
script_content += commands.map do |cmd|
|
140
|
+
<<~SHELL
|
141
|
+
echo "→ Executing: #{cmd}"
|
142
|
+
if ! #{cmd}; then
|
143
|
+
echo "✗ Command failed: #{cmd}"
|
144
|
+
raise BaseGeneratorError, "Command failed: #{cmd}"
|
145
|
+
fi
|
146
|
+
SHELL
|
147
|
+
end
|
148
|
+
|
149
|
+
script_content << 'exit 0'
|
150
|
+
|
151
|
+
ssh_cmd = ['ssh']
|
152
|
+
ssh_cmd << "-i #{options[:key_path]}" if options[:key_path]
|
153
|
+
ssh_cmd << ssh_options
|
154
|
+
ssh_cmd << ssh_destination
|
155
|
+
ssh_cmd << "'#{script_content.join("\n")}'"
|
156
|
+
|
157
|
+
success = system(ssh_cmd.join(' '))
|
158
|
+
|
159
|
+
if success
|
160
|
+
say_status 'success', 'All commands completed successfully', :green
|
161
|
+
else
|
162
|
+
say_status 'error', "Command failed with exit status #{$CHILD_STATUS.exitstatus}", :red
|
163
|
+
case $CHILD_STATUS.exitstatus
|
164
|
+
when 255
|
165
|
+
say ' → Could not connect to the server. Please check:', :red
|
166
|
+
say ' • SSH host and user are correct'
|
167
|
+
say ' • Your authentication credentials are valid'
|
168
|
+
say ' • Server is reachable and SSH port (22) is open'
|
169
|
+
when 126, 127
|
170
|
+
say ' → Command not found or not executable', :red
|
171
|
+
say ' • Ensure all required software is installed on the remote server'
|
172
|
+
end
|
173
|
+
exit $CHILD_STATUS.exitstatus
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|