plutonium 0.39.1 → 0.40.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.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium-connect-resource/SKILL.md +19 -1
- data/.claude/skills/plutonium-controller/SKILL.md +5 -9
- data/.claude/skills/plutonium-definition-query/SKILL.md +10 -2
- data/.claude/skills/plutonium-installation/SKILL.md +9 -7
- data/.claude/skills/plutonium-invites/SKILL.md +363 -0
- data/.claude/skills/plutonium-package/SKILL.md +2 -1
- data/.claude/skills/plutonium-portal/SKILL.md +30 -16
- data/.claude/skills/plutonium-rodauth/SKILL.md +111 -18
- data/CHANGELOG.md +48 -0
- data/app/assets/plutonium.css +1 -1
- data/config/initializers/sqlite_alias.rb +8 -8
- data/docs/.vitepress/config.ts +1 -0
- data/docs/getting-started/tutorial/07-author-portal.md +1 -0
- data/docs/getting-started/tutorial/08-customizing-ui.md +5 -2
- data/docs/guides/adding-resources.md +10 -0
- data/docs/guides/authentication.md +15 -8
- data/docs/guides/creating-packages.md +13 -8
- data/docs/guides/index.md +2 -0
- data/docs/guides/search-filtering.md +8 -3
- data/docs/guides/user-invites.md +497 -0
- data/docs/public/templates/base.rb +5 -1
- data/docs/public/templates/lite.rb +42 -0
- data/docs/public/templates/pluton8.rb +7 -2
- data/docs/reference/controller/index.md +12 -7
- data/docs/reference/definition/query.md +12 -3
- data/docs/reference/generators/index.md +70 -10
- data/docs/reference/portal/index.md +22 -11
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +31 -0
- data/lib/generators/pu/gem/active_shrine/templates/config/initializers/shrine.rb.tt +58 -0
- data/lib/generators/pu/gem/annotated/templates/lib/tasks/auto_annotate_models.rake +6 -1
- data/lib/generators/pu/gem/dotenv/templates/config/initializers/001_ensure_required_env.rb +3 -0
- data/lib/generators/pu/invites/USAGE +27 -0
- data/lib/generators/pu/invites/install_generator.rb +364 -0
- data/lib/generators/pu/invites/invitable/USAGE +31 -0
- data/lib/generators/pu/invites/invitable_generator.rb +143 -0
- data/lib/generators/pu/invites/templates/INSTRUCTIONS +22 -0
- data/lib/generators/pu/invites/templates/app/interactions/invite_user_interaction.rb.tt +24 -0
- data/lib/generators/pu/invites/templates/app/interactions/user_invite_user_interaction.rb.tt +26 -0
- data/lib/generators/pu/invites/templates/db/migrate/create_user_invites.rb.tt +47 -0
- data/lib/generators/pu/invites/templates/invitable/invitation.html.erb.tt +45 -0
- data/lib/generators/pu/invites/templates/invitable/invitation.text.erb.tt +15 -0
- data/lib/generators/pu/invites/templates/invitable/invite_user_interaction.rb.tt +33 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +77 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/welcome_controller.rb.tt +68 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/definitions/invites/user_invite_definition.rb.tt +23 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/interactions/invites/cancel_invite_interaction.rb.tt +7 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/interactions/invites/resend_invite_interaction.rb.tt +7 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/mailers/invites/user_invite_mailer.rb.tt +34 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/models/invites/user_invite.rb.tt +41 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/policies/invites/user_invite_policy.rb.tt +33 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/error.html.erb.tt +24 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/landing.html.erb.tt +40 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/show.html.erb.tt +39 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/signup.html.erb.tt +49 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.html.erb.tt +45 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.text.erb.tt +15 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/welcome/pending_invitation.html.erb.tt +23 -0
- data/lib/generators/pu/invites/templates/packages/invites/app/views/layouts/invites/invitation.html.erb.tt +33 -0
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +23 -2
- data/lib/generators/pu/lib/plutonium_generators/concerns/configures_sqlite.rb +130 -0
- data/lib/generators/pu/lib/plutonium_generators/concerns/mounts_engines.rb +72 -0
- data/lib/generators/pu/lib/plutonium_generators/concerns/package_selector.rb +4 -2
- data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +7 -1
- data/lib/generators/pu/lite/litestream/litestream_generator.rb +105 -0
- data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +88 -0
- data/lib/generators/pu/lite/rails_pulse/templates/config/initializers/rails_pulse.rb.tt +14 -0
- data/lib/generators/pu/lite/setup/setup_generator.rb +54 -0
- data/lib/generators/pu/lite/solid_cable/solid_cable_generator.rb +65 -0
- data/lib/generators/pu/lite/solid_cache/solid_cache_generator.rb +66 -0
- data/lib/generators/pu/lite/solid_errors/solid_errors_generator.rb +61 -0
- data/lib/generators/pu/lite/solid_queue/solid_queue_generator.rb +107 -0
- data/lib/generators/pu/pkg/portal/USAGE +8 -2
- data/lib/generators/pu/pkg/portal/portal_generator.rb +11 -1
- data/lib/generators/pu/pkg/portal/templates/app/controllers/concerns/controller.rb.tt +2 -0
- data/lib/generators/pu/pkg/portal/templates/app/controllers/plutonium_controller.rb.tt +1 -0
- data/lib/generators/pu/pkg/portal/templates/app/controllers/resource_controller.rb.tt +7 -0
- data/lib/generators/pu/pkg/portal/templates/lib/engine.rb.tt +3 -0
- data/lib/generators/pu/res/conn/USAGE +5 -0
- data/lib/generators/pu/res/conn/conn_generator.rb +30 -4
- data/lib/generators/pu/res/scaffold/scaffold_generator.rb +6 -3
- data/lib/generators/pu/res/scaffold/templates/policy.rb.tt +6 -6
- data/lib/generators/pu/rodauth/account_generator.rb +36 -11
- data/lib/generators/pu/rodauth/admin_generator.rb +55 -0
- data/lib/generators/pu/rodauth/install_generator.rb +1 -8
- data/lib/generators/pu/rodauth/templates/app/interactions/invite_admin_interaction.rb.tt +25 -0
- data/lib/generators/pu/rodauth/templates/app/models/account.rb.tt +6 -2
- data/lib/generators/pu/saas/USAGE +22 -0
- data/lib/generators/pu/saas/entity/USAGE +19 -0
- data/lib/generators/pu/saas/entity_generator.rb +55 -0
- data/lib/generators/pu/saas/membership/USAGE +25 -0
- data/lib/generators/pu/saas/membership_generator.rb +165 -0
- data/lib/generators/pu/saas/setup/USAGE +27 -0
- data/lib/generators/pu/saas/setup_generator.rb +98 -0
- data/lib/generators/pu/saas/user/USAGE +21 -0
- data/lib/generators/pu/saas/user_generator.rb +66 -0
- data/lib/plutonium/core/controller.rb +9 -5
- data/lib/plutonium/definition/base.rb +3 -1
- data/lib/plutonium/definition/scoping.rb +20 -0
- data/lib/plutonium/invites/concerns/cancel_invite.rb +44 -0
- data/lib/plutonium/invites/concerns/invitable.rb +98 -0
- data/lib/plutonium/invites/concerns/invite_token.rb +186 -0
- data/lib/plutonium/invites/concerns/invite_user.rb +147 -0
- data/lib/plutonium/invites/concerns/resend_invite.rb +66 -0
- data/lib/plutonium/invites/controller.rb +226 -0
- data/lib/plutonium/invites/pending_invite_check.rb +76 -0
- data/lib/plutonium/invites.rb +6 -0
- data/lib/plutonium/resource/controllers/queryable.rb +4 -0
- data/lib/plutonium/resource/query_object.rb +3 -5
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- metadata +64 -7
- data/lib/generators/pu/res/entity/entity_generator.rb +0 -158
- data/lib/generators/pu/rodauth/customer_generator.rb +0 -101
- data/public/plutonium-assets/plutonium-logo-original.png +0 -0
- data/public/plutonium-assets/plutonium-logo-white.png +0 -0
- data/public/plutonium-assets/plutonium-logo.png +0 -0
|
@@ -430,9 +430,14 @@ module PlutoniumGenerators
|
|
|
430
430
|
end
|
|
431
431
|
|
|
432
432
|
def bundle(*gems, **options)
|
|
433
|
-
gems = Array(gems).
|
|
433
|
+
gems = Array(gems).flatten
|
|
434
|
+
# Skip gems already in bundle
|
|
435
|
+
gems = gems.reject { |g| gem_in_bundle?(g) }
|
|
436
|
+
return if gems.empty?
|
|
437
|
+
|
|
438
|
+
gems_str = gems.join(" ")
|
|
434
439
|
options = hash_to_cli_options options
|
|
435
|
-
cmd_args = "add #{
|
|
440
|
+
cmd_args = "add #{gems_str} #{options}"
|
|
436
441
|
|
|
437
442
|
log :bundle, cmd_args
|
|
438
443
|
Bundler.with_unbundled_env do
|
|
@@ -440,6 +445,14 @@ module PlutoniumGenerators
|
|
|
440
445
|
end
|
|
441
446
|
end
|
|
442
447
|
|
|
448
|
+
def gem_in_bundle?(name)
|
|
449
|
+
in_root do
|
|
450
|
+
return true if File.exist?("Gemfile") && File.read("Gemfile").match?(/gem ['"]#{name}['"]/)
|
|
451
|
+
return true if File.exist?("Gemfile.lock") && File.read("Gemfile.lock").include?(" #{name} ")
|
|
452
|
+
end
|
|
453
|
+
false
|
|
454
|
+
end
|
|
455
|
+
|
|
443
456
|
def unbundle(*gems)
|
|
444
457
|
gems = Array(gems).join " "
|
|
445
458
|
cmd_args = "remove #{gems}"
|
|
@@ -459,6 +472,14 @@ module PlutoniumGenerators
|
|
|
459
472
|
end
|
|
460
473
|
end
|
|
461
474
|
|
|
475
|
+
def file_includes?(path, check)
|
|
476
|
+
destination = File.expand_path(path, destination_root)
|
|
477
|
+
return false unless File.exist?(destination)
|
|
478
|
+
|
|
479
|
+
content = File.read(destination)
|
|
480
|
+
check.is_a?(Regexp) ? content.match?(check) : content.include?(check)
|
|
481
|
+
end
|
|
482
|
+
|
|
462
483
|
private
|
|
463
484
|
|
|
464
485
|
#
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "psych"
|
|
4
|
+
|
|
5
|
+
module PlutoniumGenerators
|
|
6
|
+
module Concerns
|
|
7
|
+
module ConfiguresSqlite
|
|
8
|
+
class DatabaseYAML
|
|
9
|
+
COMMENTED_PROD_DATABASE = "# database: path/to/persistent/storage/production.sqlite3"
|
|
10
|
+
UNCOMMENTED_PROD_DATABASE = "database: path/to/persistent/storage/production.sqlite3"
|
|
11
|
+
|
|
12
|
+
attr_reader :content
|
|
13
|
+
|
|
14
|
+
def initialize(path:)
|
|
15
|
+
@content = File.read(path)
|
|
16
|
+
# if the production environment has the default commented database value,
|
|
17
|
+
# uncomment it so that the value can be parsed
|
|
18
|
+
@content.gsub!(COMMENTED_PROD_DATABASE, UNCOMMENTED_PROD_DATABASE)
|
|
19
|
+
@stream = Psych.parse_stream(@content)
|
|
20
|
+
@emission_stream = Psych::Nodes::Stream.new
|
|
21
|
+
@emission_document = Psych::Nodes::Document.new
|
|
22
|
+
@emission_mapping = Psych::Nodes::Mapping.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add_database(name)
|
|
26
|
+
root = @stream.children.first.root
|
|
27
|
+
root.children.each_slice(2).map do |scalar, mapping|
|
|
28
|
+
next unless scalar.is_a?(Psych::Nodes::Scalar)
|
|
29
|
+
next unless mapping.is_a?(Psych::Nodes::Mapping)
|
|
30
|
+
next unless mapping.anchor.nil? || mapping.anchor.empty?
|
|
31
|
+
next if mapping.children.each_slice(2).any? do |key, value|
|
|
32
|
+
key.is_a?(Psych::Nodes::Scalar) && key.value == name && value.is_a?(Psych::Nodes::Alias) && value.anchor == name
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
new_mapping = Psych::Nodes::Mapping.new
|
|
36
|
+
if mapping.children.first.value == "<<" # 2-tiered environment
|
|
37
|
+
new_mapping.children.concat [
|
|
38
|
+
Psych::Nodes::Scalar.new("primary"),
|
|
39
|
+
mapping,
|
|
40
|
+
Psych::Nodes::Scalar.new(name),
|
|
41
|
+
Psych::Nodes::Alias.new(name)
|
|
42
|
+
]
|
|
43
|
+
else # 3-tiered environment
|
|
44
|
+
new_mapping.children.concat mapping.children
|
|
45
|
+
new_mapping.children.concat [
|
|
46
|
+
Psych::Nodes::Scalar.new(name),
|
|
47
|
+
Psych::Nodes::Alias.new(name)
|
|
48
|
+
]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
old_environment_entry = emit_pair(scalar, mapping)
|
|
52
|
+
new_environment_entry = emit_pair(scalar, new_mapping)
|
|
53
|
+
|
|
54
|
+
[scalar.value, old_environment_entry, new_environment_entry]
|
|
55
|
+
end.compact!
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def new_database(name, migrations_paths: nil)
|
|
59
|
+
migrations_paths ||= "db/#{name}_migrate"
|
|
60
|
+
db = Psych::Nodes::Mapping.new(name)
|
|
61
|
+
db.children.concat [
|
|
62
|
+
Psych::Nodes::Scalar.new("<<"),
|
|
63
|
+
Psych::Nodes::Alias.new("default"),
|
|
64
|
+
Psych::Nodes::Scalar.new("migrations_paths"),
|
|
65
|
+
Psych::Nodes::Scalar.new(migrations_paths),
|
|
66
|
+
Psych::Nodes::Scalar.new("database"),
|
|
67
|
+
Psych::Nodes::Scalar.new("storage/<%= Rails.env %>-#{name}.sqlite3")
|
|
68
|
+
]
|
|
69
|
+
"\n" + emit_pair(Psych::Nodes::Scalar.new(name), db)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def database_def_regex(name)
|
|
73
|
+
/#{name}: &#{name}\n(?:[ \t]+.*\n)+/
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private
|
|
77
|
+
|
|
78
|
+
def emit_pair(scalar, mapping)
|
|
79
|
+
@emission_mapping.children.clear.concat [scalar, mapping]
|
|
80
|
+
@emission_document.children.clear.concat [@emission_mapping]
|
|
81
|
+
@emission_stream.children.clear.concat [@emission_document]
|
|
82
|
+
output = @emission_stream.yaml.gsub!(/^---/, "").strip!
|
|
83
|
+
output.gsub!(UNCOMMENTED_PROD_DATABASE, COMMENTED_PROD_DATABASE)
|
|
84
|
+
output
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def database_yaml
|
|
91
|
+
@database_yaml ||= DatabaseYAML.new(path: File.expand_path("config/database.yml", destination_root))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def add_sqlite_database(name, migrations_paths: nil)
|
|
95
|
+
# Define the new database configuration
|
|
96
|
+
insert_into_file "config/database.yml",
|
|
97
|
+
database_yaml.new_database(name, migrations_paths: migrations_paths) + "\n",
|
|
98
|
+
after: database_yaml.database_def_regex("default"),
|
|
99
|
+
verbose: false,
|
|
100
|
+
force: false
|
|
101
|
+
say_status :def_db, "#{name} (database.yml)"
|
|
102
|
+
|
|
103
|
+
# Add the new database to all environments
|
|
104
|
+
database_yaml.add_database(name)&.each do |environment, old_entry, new_entry|
|
|
105
|
+
gsub_file "config/database.yml", old_entry, new_entry, verbose: false
|
|
106
|
+
say_status :add_db, "#{name} -> #{environment} (database.yml)"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def prepare_database(name)
|
|
111
|
+
Bundler.with_unbundled_env do
|
|
112
|
+
run "bin/rails db:prepare", env: {"DATABASE" => name}
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def add_application_config(config_line, after_pattern: nil)
|
|
117
|
+
return if file_includes?("config/application.rb", config_line)
|
|
118
|
+
|
|
119
|
+
pattern = after_pattern || /^([ \t]*).*?(?=\n\s*end\nend)$/
|
|
120
|
+
insert_into_file "config/application.rb", after: pattern do
|
|
121
|
+
if after_pattern
|
|
122
|
+
"\n\\1#{config_line}"
|
|
123
|
+
else
|
|
124
|
+
"\n\n\\1#{config_line}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PlutoniumGenerators
|
|
4
|
+
module Concerns
|
|
5
|
+
module MountsEngines
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def mount_engine(engine_mount, route_file: "config/routes.rb", authenticated: false)
|
|
9
|
+
return if file_includes?(route_file, engine_mount)
|
|
10
|
+
|
|
11
|
+
if authenticated
|
|
12
|
+
mount_authenticated_engine(engine_mount, route_file)
|
|
13
|
+
else
|
|
14
|
+
insert_into_file route_file, before: /^end\s*\z/ do
|
|
15
|
+
" #{engine_mount}\n"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def mount_authenticated_engine(engine_mount, route_file)
|
|
21
|
+
ensure_management_constraint
|
|
22
|
+
|
|
23
|
+
content = File.read(File.expand_path(route_file, destination_root))
|
|
24
|
+
# Match constraint block opening - use [ \t]* instead of \s* to avoid matching newlines
|
|
25
|
+
constraint_match = content.match(/^(\s*)constraints ManagementConstraint do[ \t]*\n/)
|
|
26
|
+
|
|
27
|
+
if constraint_match
|
|
28
|
+
indent = constraint_match[1]
|
|
29
|
+
# Insert after the opening line (not including any subsequent blank lines)
|
|
30
|
+
insert_into_file route_file, after: /^#{Regexp.escape(indent)}constraints ManagementConstraint do[ \t]*\n/ do
|
|
31
|
+
"#{indent} #{engine_mount}\n"
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
# Create new constraint block before the final end
|
|
35
|
+
insert_into_file route_file, before: /^end\s*\z/ do
|
|
36
|
+
" constraints ManagementConstraint do\n #{engine_mount}\n end\n"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def ensure_management_constraint
|
|
42
|
+
constraint_file = "app/constraints/management_constraint.rb"
|
|
43
|
+
return if File.exist?(File.expand_path(constraint_file, destination_root))
|
|
44
|
+
|
|
45
|
+
create_file constraint_file, <<~RUBY
|
|
46
|
+
# frozen_string_literal: true
|
|
47
|
+
|
|
48
|
+
class ManagementConstraint
|
|
49
|
+
def self.matches?(request)
|
|
50
|
+
false # TODO: Implement authentication
|
|
51
|
+
# Examples:
|
|
52
|
+
# Rodauth: request.env["rodauth.admin"]&.logged_in?
|
|
53
|
+
# Devise: request.env["warden"].user(:admin).present?
|
|
54
|
+
# Custom: request.session[:admin_id].present?
|
|
55
|
+
# HTTP Basic: authenticate_with_http_basic(request)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# HTTP Basic Auth example:
|
|
59
|
+
# def self.authenticate_with_http_basic(request)
|
|
60
|
+
# auth = Rack::Auth::Basic::Request.new(request.env)
|
|
61
|
+
# return false unless auth.provided? && auth.basic?
|
|
62
|
+
#
|
|
63
|
+
# username, password = auth.credentials
|
|
64
|
+
# ActiveSupport::SecurityUtils.secure_compare(username, ENV["ADMIN_USERNAME"]) &&
|
|
65
|
+
# ActiveSupport::SecurityUtils.secure_compare(password, ENV["ADMIN_PASSWORD"])
|
|
66
|
+
# end
|
|
67
|
+
end
|
|
68
|
+
RUBY
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -37,8 +37,10 @@ module PlutoniumGenerators
|
|
|
37
37
|
|
|
38
38
|
def select_package(selected_package = nil, msg: "Select package", pkgs: nil)
|
|
39
39
|
pkgs ||= available_packages
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
# Normalize input to underscore format (e.g., "CustomerPortal" -> "customer_portal")
|
|
41
|
+
normalized = selected_package&.underscore
|
|
42
|
+
if pkgs.include?(normalized)
|
|
43
|
+
normalized
|
|
42
44
|
else
|
|
43
45
|
prompt.select(msg, pkgs)
|
|
44
46
|
end
|
|
@@ -39,7 +39,13 @@ module PlutoniumGenerators
|
|
|
39
39
|
def name
|
|
40
40
|
@pu_name ||= begin
|
|
41
41
|
@original_name = @name
|
|
42
|
-
|
|
42
|
+
resource_name = super.singularize.underscore
|
|
43
|
+
dest_namespace = main_app? ? nil : selected_destination_feature.underscore
|
|
44
|
+
# Strip destination namespace from resource name if already present
|
|
45
|
+
if dest_namespace && resource_name.start_with?("#{dest_namespace}/")
|
|
46
|
+
resource_name = resource_name.sub("#{dest_namespace}/", "")
|
|
47
|
+
end
|
|
48
|
+
@name = [dest_namespace, resource_name].compact.join "/"
|
|
43
49
|
set_destination_root!
|
|
44
50
|
@name
|
|
45
51
|
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../lib/plutonium_generators"
|
|
4
|
+
|
|
5
|
+
module Pu
|
|
6
|
+
module Lite
|
|
7
|
+
class LitestreamGenerator < Rails::Generators::Base
|
|
8
|
+
include PlutoniumGenerators::Generator
|
|
9
|
+
include PlutoniumGenerators::Concerns::MountsEngines
|
|
10
|
+
|
|
11
|
+
desc "Set up Litestream for SQLite replication/backup"
|
|
12
|
+
|
|
13
|
+
class_option :route, type: :string, default: "/manage/litestream",
|
|
14
|
+
desc: "Route path for Litestream UI"
|
|
15
|
+
class_option :credentials, type: :boolean, default: true,
|
|
16
|
+
desc: "Configure Litestream to use Rails credentials"
|
|
17
|
+
|
|
18
|
+
def start
|
|
19
|
+
bundle "litestream"
|
|
20
|
+
run_litestream_install
|
|
21
|
+
create_litestream_script
|
|
22
|
+
configure_kamal
|
|
23
|
+
mount_litestream_engine
|
|
24
|
+
configure_litestream_initializer
|
|
25
|
+
show_instructions
|
|
26
|
+
rescue => e
|
|
27
|
+
exception "#{self.class} failed:", e
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def run_litestream_install
|
|
33
|
+
Bundler.with_unbundled_env do
|
|
34
|
+
run "bin/rails generate litestream:install"
|
|
35
|
+
end
|
|
36
|
+
# Remove puma plugin added by litestream:install
|
|
37
|
+
run "git checkout -- config/puma.rb 2>/dev/null || true"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def create_litestream_script
|
|
41
|
+
litestream_script = "bin/litestream"
|
|
42
|
+
return if File.exist?(File.expand_path(litestream_script, destination_root))
|
|
43
|
+
|
|
44
|
+
create_file litestream_script, <<~BASH
|
|
45
|
+
#!/usr/bin/env bash
|
|
46
|
+
set -e
|
|
47
|
+
exec bundle exec litestream replicate -config config/litestream.yml
|
|
48
|
+
BASH
|
|
49
|
+
chmod litestream_script, 0o755
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def configure_kamal
|
|
53
|
+
deploy_file = "config/deploy.yml"
|
|
54
|
+
return unless File.exist?(File.expand_path(deploy_file, destination_root))
|
|
55
|
+
return if file_includes?(deploy_file, "litestream:")
|
|
56
|
+
|
|
57
|
+
insert_into_file deploy_file, after: /^servers:.*\n/ do
|
|
58
|
+
<<~YAML
|
|
59
|
+
litestream:
|
|
60
|
+
hosts:
|
|
61
|
+
- <%= ENV['DEPLOY_HOST'] %>
|
|
62
|
+
cmd: bin/litestream
|
|
63
|
+
YAML
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def mount_litestream_engine
|
|
68
|
+
mount_engine %(mount Litestream::Engine, at: "#{options[:route]}"), authenticated: true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def configure_litestream_initializer
|
|
72
|
+
return unless options[:credentials]
|
|
73
|
+
|
|
74
|
+
initializer_file = "config/initializers/litestream.rb"
|
|
75
|
+
uncomment_lines initializer_file, /litestream_credentials/
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def show_instructions
|
|
79
|
+
say ""
|
|
80
|
+
say "Litestream Setup Instructions:", :green
|
|
81
|
+
say "=" * 50
|
|
82
|
+
say ""
|
|
83
|
+
say "Litestream requires an S3-compatible storage provider (AWS S3, DigitalOcean Spaces, etc.)"
|
|
84
|
+
say ""
|
|
85
|
+
|
|
86
|
+
if options[:credentials]
|
|
87
|
+
say "Edit your credentials to store bucket details:"
|
|
88
|
+
say " bin/rails credentials:edit"
|
|
89
|
+
say ""
|
|
90
|
+
say "Add the following:"
|
|
91
|
+
say " litestream:"
|
|
92
|
+
say " replica_bucket: <your-bucket-name>"
|
|
93
|
+
say " replica_key_id: <public-key>"
|
|
94
|
+
say " replica_access_key: <private-key>"
|
|
95
|
+
say ""
|
|
96
|
+
say "Verify configuration:"
|
|
97
|
+
say " bin/rails litestream:env"
|
|
98
|
+
else
|
|
99
|
+
say "Configure Litestream in: config/initializers/litestream.rb"
|
|
100
|
+
end
|
|
101
|
+
say ""
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../lib/plutonium_generators"
|
|
4
|
+
|
|
5
|
+
module Pu
|
|
6
|
+
module Lite
|
|
7
|
+
class RailsPulseGenerator < Rails::Generators::Base
|
|
8
|
+
include PlutoniumGenerators::Generator
|
|
9
|
+
include PlutoniumGenerators::Concerns::ConfiguresSqlite
|
|
10
|
+
include PlutoniumGenerators::Concerns::MountsEngines
|
|
11
|
+
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
|
|
14
|
+
desc "Set up Rails Pulse for performance monitoring with SQLite"
|
|
15
|
+
|
|
16
|
+
class_option :database, type: :string, default: "rails_pulse",
|
|
17
|
+
desc: "Database name for Rails Pulse (default: rails_pulse)"
|
|
18
|
+
class_option :route, type: :string, default: "/manage/pulse",
|
|
19
|
+
desc: "Route path for Rails Pulse dashboard"
|
|
20
|
+
|
|
21
|
+
def start
|
|
22
|
+
bundle "rails_pulse"
|
|
23
|
+
|
|
24
|
+
if options[:database]
|
|
25
|
+
setup_separate_database
|
|
26
|
+
else
|
|
27
|
+
run_rails_pulse_install
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
template "config/initializers/rails_pulse.rb", force: true
|
|
31
|
+
mount_rails_pulse_engine
|
|
32
|
+
setup_recurring_tasks if solid_queue_installed?
|
|
33
|
+
rescue => e
|
|
34
|
+
exception "#{self.class} failed:", e
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def run_rails_pulse_install
|
|
40
|
+
Bundler.with_unbundled_env do
|
|
41
|
+
run "bin/rails generate rails_pulse:install"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def setup_separate_database
|
|
46
|
+
@db_name = options[:database]
|
|
47
|
+
|
|
48
|
+
# Run install first - creates schema file and migration
|
|
49
|
+
Bundler.with_unbundled_env do
|
|
50
|
+
run "bin/rails generate rails_pulse:install --database=separate"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Then add database config
|
|
54
|
+
add_sqlite_database(@db_name, migrations_paths: "db/rails_pulse_migrate")
|
|
55
|
+
|
|
56
|
+
# Finally prepare the database (runs migration that loads schema)
|
|
57
|
+
prepare_database(@db_name)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def mount_rails_pulse_engine
|
|
61
|
+
mount_engine %(mount RailsPulse::Engine, at: "#{options[:route]}"), authenticated: true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def solid_queue_installed?
|
|
65
|
+
gem_in_bundle?("solid_queue")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def setup_recurring_tasks
|
|
69
|
+
recurring_file = "config/recurring.yml"
|
|
70
|
+
return unless File.exist?(File.expand_path(recurring_file, destination_root))
|
|
71
|
+
return if file_includes?(recurring_file, "rails_pulse")
|
|
72
|
+
|
|
73
|
+
recurring_tasks = <<~YAML
|
|
74
|
+
|
|
75
|
+
rails_pulse_summary:
|
|
76
|
+
class: RailsPulse::SummaryJob
|
|
77
|
+
schedule: "5 * * * *" # 5 minutes past every hour
|
|
78
|
+
|
|
79
|
+
rails_pulse_cleanup:
|
|
80
|
+
class: RailsPulse::CleanupJob
|
|
81
|
+
schedule: "0 1 * * *" # Daily at 1am
|
|
82
|
+
YAML
|
|
83
|
+
|
|
84
|
+
append_to_file recurring_file, recurring_tasks
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RailsPulse.configure do |config|
|
|
4
|
+
# Enable/disable Rails Pulse globally
|
|
5
|
+
config.enabled = Rails.env.production? || Rails.env.development?
|
|
6
|
+
|
|
7
|
+
# Asset tracking (disable to reduce noise)
|
|
8
|
+
config.track_assets = false
|
|
9
|
+
<%- if options[:database] -%>
|
|
10
|
+
|
|
11
|
+
# Use separate database for performance data
|
|
12
|
+
config.connects_to = {database: {writing: :<%= options[:database] %>}}
|
|
13
|
+
<%- end -%>
|
|
14
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../lib/plutonium_generators"
|
|
4
|
+
|
|
5
|
+
module Pu
|
|
6
|
+
module Lite
|
|
7
|
+
class SetupGenerator < Rails::Generators::Base
|
|
8
|
+
include PlutoniumGenerators::Generator
|
|
9
|
+
|
|
10
|
+
desc "Set up SQLite with proper configuration"
|
|
11
|
+
|
|
12
|
+
RAILS_8_VERSION = ::Gem::Version.new("8.0.0")
|
|
13
|
+
|
|
14
|
+
def start
|
|
15
|
+
ensure_sqlite3_version
|
|
16
|
+
|
|
17
|
+
# Add enhanced adapter for Rails 7
|
|
18
|
+
if rails_version < RAILS_8_VERSION
|
|
19
|
+
bundle "activerecord-enhancedsqlite3-adapter", version: "~> 0.8.0"
|
|
20
|
+
end
|
|
21
|
+
rescue => e
|
|
22
|
+
exception "#{self.class} failed:", e
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
SQLITE3_MIN_VERSION = ::Gem::Version.new("2.0.0")
|
|
28
|
+
|
|
29
|
+
def ensure_sqlite3_version
|
|
30
|
+
current = installed_gem_version("sqlite3")
|
|
31
|
+
if current.nil?
|
|
32
|
+
bundle "sqlite3", version: "~> 2.0"
|
|
33
|
+
elsif current < SQLITE3_MIN_VERSION
|
|
34
|
+
log :bundle, "updating sqlite3 from #{current} to ~> 2.0"
|
|
35
|
+
gsub_file "Gemfile", /^gem ["']sqlite3["'].*$/, 'gem "sqlite3", "~> 2.0"'
|
|
36
|
+
bundle!
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def installed_gem_version(gem_name)
|
|
41
|
+
lockfile = File.join(destination_root, "Gemfile.lock")
|
|
42
|
+
return nil unless File.exist?(lockfile)
|
|
43
|
+
|
|
44
|
+
content = File.read(lockfile)
|
|
45
|
+
match = content.match(/#{gem_name} \((\d+\.\d+\.\d+)/)
|
|
46
|
+
::Gem::Version.new(match[1]) if match
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def rails_version
|
|
50
|
+
@rails_version ||= ::Gem::Version.new(Rails::VERSION::STRING).release
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../lib/plutonium_generators"
|
|
4
|
+
|
|
5
|
+
module Pu
|
|
6
|
+
module Lite
|
|
7
|
+
class SolidCableGenerator < Rails::Generators::Base
|
|
8
|
+
include PlutoniumGenerators::Generator
|
|
9
|
+
include PlutoniumGenerators::Concerns::ConfiguresSqlite
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Set up Solid Cable for Action Cable with SQLite"
|
|
14
|
+
|
|
15
|
+
class_option :database, type: :string, default: "cable",
|
|
16
|
+
desc: "Database name for Solid Cable"
|
|
17
|
+
|
|
18
|
+
def start
|
|
19
|
+
@db_name = options[:database]
|
|
20
|
+
|
|
21
|
+
bundle "solid_cable"
|
|
22
|
+
add_sqlite_database(@db_name)
|
|
23
|
+
run_solid_cable_install
|
|
24
|
+
configure_cable_yml
|
|
25
|
+
prepare_database(@db_name)
|
|
26
|
+
rescue => e
|
|
27
|
+
exception "#{self.class} failed:", e
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def run_solid_cable_install
|
|
33
|
+
Bundler.with_unbundled_env do
|
|
34
|
+
run "bin/rails generate solid_cable:install", env: {"DATABASE" => @db_name}
|
|
35
|
+
end
|
|
36
|
+
run "git checkout -- config/environments/production.rb 2>/dev/null || true"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def configure_cable_yml
|
|
40
|
+
cable_file = "config/cable.yml"
|
|
41
|
+
remove_file cable_file
|
|
42
|
+
create_file cable_file, <<~YAML
|
|
43
|
+
default: &default
|
|
44
|
+
adapter: solid_cable
|
|
45
|
+
polling_interval: 1.second
|
|
46
|
+
keep_messages_around_for: 1.day
|
|
47
|
+
connects_to:
|
|
48
|
+
database:
|
|
49
|
+
writing: #{@db_name}
|
|
50
|
+
|
|
51
|
+
development:
|
|
52
|
+
<<: *default
|
|
53
|
+
silence_polling: true
|
|
54
|
+
|
|
55
|
+
test:
|
|
56
|
+
<<: *default
|
|
57
|
+
|
|
58
|
+
production:
|
|
59
|
+
<<: *default
|
|
60
|
+
polling_interval: 0.1.seconds
|
|
61
|
+
YAML
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../lib/plutonium_generators"
|
|
4
|
+
|
|
5
|
+
module Pu
|
|
6
|
+
module Lite
|
|
7
|
+
class SolidCacheGenerator < Rails::Generators::Base
|
|
8
|
+
include PlutoniumGenerators::Generator
|
|
9
|
+
include PlutoniumGenerators::Concerns::ConfiguresSqlite
|
|
10
|
+
|
|
11
|
+
source_root File.expand_path("templates", __dir__)
|
|
12
|
+
|
|
13
|
+
desc "Set up Solid Cache with SQLite"
|
|
14
|
+
|
|
15
|
+
class_option :database, type: :string, default: "cache",
|
|
16
|
+
desc: "Database name for Solid Cache"
|
|
17
|
+
class_option :dev_cache, type: :boolean, default: true,
|
|
18
|
+
desc: "Enable caching in development"
|
|
19
|
+
|
|
20
|
+
def start
|
|
21
|
+
@db_name = options[:database]
|
|
22
|
+
|
|
23
|
+
bundle "solid_cache"
|
|
24
|
+
add_sqlite_database(@db_name)
|
|
25
|
+
run_solid_cache_install
|
|
26
|
+
configure_cache_yml
|
|
27
|
+
configure_application
|
|
28
|
+
prepare_database(@db_name)
|
|
29
|
+
enable_dev_cache if options[:dev_cache]
|
|
30
|
+
rescue => e
|
|
31
|
+
exception "#{self.class} failed:", e
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def run_solid_cache_install
|
|
37
|
+
Bundler.with_unbundled_env do
|
|
38
|
+
run "bin/rails generate solid_cache:install", env: {"DATABASE" => @db_name}
|
|
39
|
+
end
|
|
40
|
+
run "git checkout -- config/environments/production.rb 2>/dev/null || true"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def configure_cache_yml
|
|
44
|
+
cache_file = "config/cache.yml"
|
|
45
|
+
gsub_file cache_file, "database: <%= Rails.env %>", "database: #{@db_name}"
|
|
46
|
+
gsub_file cache_file, "database: cache", "database: #{@db_name}"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def configure_application
|
|
50
|
+
create_file "config/initializers/solid_cache.rb", <<~RUBY
|
|
51
|
+
# frozen_string_literal: true
|
|
52
|
+
|
|
53
|
+
Rails.application.configure do
|
|
54
|
+
config.cache_store = :solid_cache_store
|
|
55
|
+
end
|
|
56
|
+
RUBY
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def enable_dev_cache
|
|
60
|
+
Bundler.with_unbundled_env do
|
|
61
|
+
run "bin/rails dev:cache"
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|