ruby_cms 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.
- checksums.yaml +7 -0
- data/.cursor/dhh.mdc +698 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/README.md +235 -0
- data/Rakefile +30 -0
- data/app/components/ruby_cms/admin/admin_page/admin_table_content.rb +32 -0
- data/app/components/ruby_cms/admin/admin_page.rb +345 -0
- data/app/components/ruby_cms/admin/base_component.rb +78 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table.rb +149 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_actions.rb +127 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_body.rb +15 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_cell.rb +41 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_checkbox_head.rb +33 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_delete_modal.rb +174 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header.rb +59 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_header_bar.rb +159 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_pagination.rb +192 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_action_table_row.rb +97 -0
- data/app/components/ruby_cms/admin/bulk_action_table/bulk_actions.rb +137 -0
- data/app/controllers/concerns/ruby_cms/admin_pagination.rb +120 -0
- data/app/controllers/concerns/ruby_cms/admin_turbo_table.rb +68 -0
- data/app/controllers/concerns/ruby_cms/page_tracking.rb +52 -0
- data/app/controllers/concerns/ruby_cms/visitor_error_capture.rb +39 -0
- data/app/controllers/ruby_cms/admin/analytics_controller.rb +191 -0
- data/app/controllers/ruby_cms/admin/base_controller.rb +105 -0
- data/app/controllers/ruby_cms/admin/content_blocks_controller.rb +390 -0
- data/app/controllers/ruby_cms/admin/dashboard_controller.rb +50 -0
- data/app/controllers/ruby_cms/admin/locale_controller.rb +20 -0
- data/app/controllers/ruby_cms/admin/permissions_controller.rb +66 -0
- data/app/controllers/ruby_cms/admin/settings_controller.rb +223 -0
- data/app/controllers/ruby_cms/admin/user_permissions_controller.rb +59 -0
- data/app/controllers/ruby_cms/admin/users_controller.rb +107 -0
- data/app/controllers/ruby_cms/admin/visitor_errors_controller.rb +89 -0
- data/app/controllers/ruby_cms/admin/visual_editor_controller.rb +322 -0
- data/app/controllers/ruby_cms/errors_controller.rb +35 -0
- data/app/helpers/ruby_cms/admin/admin_page_helper.rb +21 -0
- data/app/helpers/ruby_cms/admin/bulk_action_table_helper.rb +159 -0
- data/app/helpers/ruby_cms/application_helper.rb +41 -0
- data/app/helpers/ruby_cms/bulk_action_table_helper.rb +151 -0
- data/app/helpers/ruby_cms/content_blocks_helper.rb +375 -0
- data/app/helpers/ruby_cms/settings_helper.rb +160 -0
- data/app/javascript/controllers/ruby_cms/auto_save_preference_controller.js +73 -0
- data/app/javascript/controllers/ruby_cms/bulk_action_table_controller.js +553 -0
- data/app/javascript/controllers/ruby_cms/clickable_row_controller.js +28 -0
- data/app/javascript/controllers/ruby_cms/flash_messages_controller.js +29 -0
- data/app/javascript/controllers/ruby_cms/index.js +104 -0
- data/app/javascript/controllers/ruby_cms/locale_tabs_controller.js +34 -0
- data/app/javascript/controllers/ruby_cms/mobile_menu_controller.js +55 -0
- data/app/javascript/controllers/ruby_cms/nav_order_sortable_controller.js +192 -0
- data/app/javascript/controllers/ruby_cms/page_preview_controller.js +135 -0
- data/app/javascript/controllers/ruby_cms/toggle_controller.js +39 -0
- data/app/javascript/controllers/ruby_cms/visual_editor_controller.js +321 -0
- data/app/models/concerns/content_block/publishable.rb +54 -0
- data/app/models/concerns/content_block/searchable.rb +22 -0
- data/app/models/content_block.rb +155 -0
- data/app/models/ruby_cms/content_block.rb +8 -0
- data/app/models/ruby_cms/permission.rb +28 -0
- data/app/models/ruby_cms/permittable.rb +39 -0
- data/app/models/ruby_cms/preference.rb +111 -0
- data/app/models/ruby_cms/user_permission.rb +12 -0
- data/app/models/ruby_cms/visitor_error.rb +109 -0
- data/app/services/ruby_cms/analytics/report.rb +362 -0
- data/app/services/ruby_cms/security_tracker.rb +92 -0
- data/app/views/layouts/ruby_cms/_admin_flash_messages.html.erb +37 -0
- data/app/views/layouts/ruby_cms/_admin_sidebar.html.erb +121 -0
- data/app/views/layouts/ruby_cms/admin.html.erb +81 -0
- data/app/views/layouts/ruby_cms/minimal.html.erb +181 -0
- data/app/views/ruby_cms/admin/analytics/index.html.erb +160 -0
- data/app/views/ruby_cms/admin/analytics/page_details.html.erb +84 -0
- data/app/views/ruby_cms/admin/analytics/partials/_back_button.html.erb +3 -0
- data/app/views/ruby_cms/admin/analytics/partials/_browser_device.html.erb +40 -0
- data/app/views/ruby_cms/admin/analytics/partials/_daily_activity_chart.html.erb +58 -0
- data/app/views/ruby_cms/admin/analytics/partials/_hourly_activity_chart.html.erb +51 -0
- data/app/views/ruby_cms/admin/analytics/partials/_recent_activity.html.erb +31 -0
- data/app/views/ruby_cms/admin/analytics/partials/_security_alert.html.erb +4 -0
- data/app/views/ruby_cms/admin/analytics/partials/_top_referrers.html.erb +21 -0
- data/app/views/ruby_cms/admin/analytics/visitor_details.html.erb +125 -0
- data/app/views/ruby_cms/admin/content_blocks/_form.html.erb +161 -0
- data/app/views/ruby_cms/admin/content_blocks/_row.html.erb +25 -0
- data/app/views/ruby_cms/admin/content_blocks/edit.html.erb +17 -0
- data/app/views/ruby_cms/admin/content_blocks/index.html.erb +66 -0
- data/app/views/ruby_cms/admin/content_blocks/new.html.erb +5 -0
- data/app/views/ruby_cms/admin/content_blocks/show.html.erb +110 -0
- data/app/views/ruby_cms/admin/dashboard/index.html.erb +198 -0
- data/app/views/ruby_cms/admin/permissions/_row.html.erb +11 -0
- data/app/views/ruby_cms/admin/permissions/index.html.erb +62 -0
- data/app/views/ruby_cms/admin/settings/index.html.erb +220 -0
- data/app/views/ruby_cms/admin/shared/_bulk_action_table_index.html.erb +56 -0
- data/app/views/ruby_cms/admin/user_permissions/index.html.erb +55 -0
- data/app/views/ruby_cms/admin/users/_row.html.erb +14 -0
- data/app/views/ruby_cms/admin/users/index.html.erb +70 -0
- data/app/views/ruby_cms/admin/visitor_errors/_row.html.erb +35 -0
- data/app/views/ruby_cms/admin/visitor_errors/index.html.erb +57 -0
- data/app/views/ruby_cms/admin/visitor_errors/show.html.erb +147 -0
- data/app/views/ruby_cms/admin/visual_editor/index.html.erb +144 -0
- data/app/views/ruby_cms/errors/not_found.html.erb +92 -0
- data/config/database.yml +6 -0
- data/config/importmap.rb +36 -0
- data/config/locales/en.yml +101 -0
- data/config/routes.rb +65 -0
- data/db/migrate/20260125000001_create_ruby_cms_permissions.rb +14 -0
- data/db/migrate/20260125000002_create_ruby_cms_user_permissions.rb +14 -0
- data/db/migrate/20260125000003_create_ruby_cms_content_blocks.rb +19 -0
- data/db/migrate/20260125000010_add_indexes_to_ruby_cms_tables.rb +9 -0
- data/db/migrate/20260127000001_add_locale_to_ruby_cms_content_blocks.rb +34 -0
- data/db/migrate/20260129000001_create_ruby_cms_visitor_errors.rb +24 -0
- data/db/migrate/20260130000001_add_referer_and_query_to_ruby_cms_visitor_errors.rb +8 -0
- data/db/migrate/20260130000002_create_ruby_cms_preferences.rb +16 -0
- data/db/migrate/20260130000003_add_category_to_ruby_cms_preferences.rb +8 -0
- data/db/migrate/20260211000001_add_ruby_cms_analytics_fields_to_ahoy_events.rb +19 -0
- data/db/migrate/20260212000001_use_unprefixed_cms_tables.rb +146 -0
- data/exe/ruby_cms +25 -0
- data/lib/generators/ruby_cms/install_generator.rb +1062 -0
- data/lib/generators/ruby_cms/templates/admin.html.erb +82 -0
- data/lib/generators/ruby_cms/templates/ruby_cms.rb +86 -0
- data/lib/ruby_cms/app_integration.rb +82 -0
- data/lib/ruby_cms/cli.rb +169 -0
- data/lib/ruby_cms/content_blocks_grouping.rb +41 -0
- data/lib/ruby_cms/content_blocks_sync.rb +329 -0
- data/lib/ruby_cms/css_compiler.rb +35 -0
- data/lib/ruby_cms/engine.rb +498 -0
- data/lib/ruby_cms/settings.rb +145 -0
- data/lib/ruby_cms/settings_registry.rb +289 -0
- data/lib/ruby_cms/version.rb +5 -0
- data/lib/ruby_cms.rb +195 -0
- data/lib/tasks/ruby_cms.rake +27 -0
- data/log/test.log +17875 -0
- data/sig/ruby_cms.rbs +4 -0
- metadata +223 -0
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module RubyCms
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
|
9
|
+
|
|
10
|
+
NEXT_STEPS_MESSAGE = <<~TEXT
|
|
11
|
+
|
|
12
|
+
✓ RubyCMS install complete.
|
|
13
|
+
|
|
14
|
+
Next steps (if not already done):
|
|
15
|
+
- rails db:migrate
|
|
16
|
+
- rails ruby_cms:seed_permissions (includes manage_visitor_errors)
|
|
17
|
+
- rails ruby_cms:setup_admin (or: rails ruby_cms:grant_manage_admin email=you@example.com)
|
|
18
|
+
- To seed content blocks from YAML: add content under content_blocks in config/locales/<locale>.yml, then run rails ruby_cms:content_blocks:seed (or call it from db/seeds.rb).
|
|
19
|
+
|
|
20
|
+
Notes:
|
|
21
|
+
- If the host uses /admin already, remove or change those routes.
|
|
22
|
+
- Avoid root to: redirect("/admin") — use a real root or ruby_cms.unauthorized_redirect_path.
|
|
23
|
+
- Review config/initializers/ruby_cms.rb (session, CSP).
|
|
24
|
+
- Add 'css: bin/rails tailwindcss:watch' to Procfile.dev for Tailwind in development.
|
|
25
|
+
- Visit /admin (sign in as the admin you configured).
|
|
26
|
+
|
|
27
|
+
Tracking:
|
|
28
|
+
- Visitor errors: Automatically captured via ApplicationController (see /admin/visitor_errors)
|
|
29
|
+
- Page views (Ahoy): Include RubyCms::PageTracking in your public controllers to track page views
|
|
30
|
+
Example: class PagesController < ApplicationController; include RubyCms::PageTracking; end
|
|
31
|
+
- Analytics: View visit/event data in Ahoy tables (ahoy_visits, ahoy_events)
|
|
32
|
+
TEXT
|
|
33
|
+
|
|
34
|
+
def run_authentication
|
|
35
|
+
user_path = Rails.root.join("app/models/user.rb")
|
|
36
|
+
return if File.exist?(user_path)
|
|
37
|
+
|
|
38
|
+
say "ℹ Task authentication: User model not found. " \
|
|
39
|
+
"Running 'rails g authentication' (Rails 8+).", :cyan
|
|
40
|
+
@authentication_attempted = true
|
|
41
|
+
run "bin/rails generate authentication"
|
|
42
|
+
run "bundle install"
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
say "⚠ Could not run 'rails g authentication': #{e.message}.", :yellow
|
|
45
|
+
say " On Rails 8+, run 'rails g authentication' and 'bundle install' manually.", :yellow
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def verify_auth
|
|
49
|
+
verify_user_model
|
|
50
|
+
verify_session_model
|
|
51
|
+
verify_application_controller
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def verify_user_model
|
|
55
|
+
return if defined?(::User)
|
|
56
|
+
|
|
57
|
+
message = if @authentication_attempted
|
|
58
|
+
"Run 'rails db:migrate' if the authentication generator succeeded."
|
|
59
|
+
else
|
|
60
|
+
"User model not found. Run 'rails g authentication' before using /admin."
|
|
61
|
+
end
|
|
62
|
+
say "ℹ Task authentication: #{message}", :yellow
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def verify_session_model
|
|
66
|
+
return if defined?(::Session)
|
|
67
|
+
|
|
68
|
+
say "ℹ Task authentication: Session model not found. " \
|
|
69
|
+
"The host app should provide authentication.",
|
|
70
|
+
:yellow
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def verify_application_controller
|
|
74
|
+
ac_path = Rails.root.join("app/controllers/application_controller.rb")
|
|
75
|
+
return unless ac_path.exist?
|
|
76
|
+
|
|
77
|
+
content = File.read(ac_path)
|
|
78
|
+
return if content.include?("include Authentication")
|
|
79
|
+
return if @authentication_warning_shown
|
|
80
|
+
|
|
81
|
+
say "ℹ Task authentication: ApplicationController does not include " \
|
|
82
|
+
"Authentication. Ensure /admin is protected.",
|
|
83
|
+
:yellow
|
|
84
|
+
@authentication_warning_shown = true
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def create_initializer
|
|
88
|
+
@detected_pages = detect_page_templates
|
|
89
|
+
template "ruby_cms.rb", "config/initializers/ruby_cms.rb"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def mount_engine
|
|
93
|
+
route 'mount RubyCms::Engine => "/"'
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def add_catch_all_route
|
|
97
|
+
routes_path = Rails.root.join("config/routes.rb")
|
|
98
|
+
return unless routes_path.exist?
|
|
99
|
+
|
|
100
|
+
content = File.read(routes_path)
|
|
101
|
+
return if content.include?("ruby_cms/errors#not_found")
|
|
102
|
+
|
|
103
|
+
# Add catch-all route at the end of the routes block (before final 'end')
|
|
104
|
+
catch_all = <<~ROUTE
|
|
105
|
+
|
|
106
|
+
# RubyCMS: Catch-all route for 404 error tracking (must be LAST)
|
|
107
|
+
match "*path", to: "ruby_cms/errors#not_found", via: :all,
|
|
108
|
+
constraints: ->(req) { !req.path.start_with?("/rails/", "/assets/") }
|
|
109
|
+
ROUTE
|
|
110
|
+
|
|
111
|
+
# Insert before the last 'end' in the file
|
|
112
|
+
gsub_file routes_path, /(\nend)\s*\z/ do
|
|
113
|
+
"#{catch_all}end\n"
|
|
114
|
+
end
|
|
115
|
+
say "✓ Catch-all route: Added for 404 error tracking", :green
|
|
116
|
+
rescue StandardError => e
|
|
117
|
+
say "⚠ Catch-all route: Could not add automatically: #{e.message}. " \
|
|
118
|
+
"Add manually at the END of routes.rb:\n " \
|
|
119
|
+
'match "*path", to: "ruby_cms/errors#not_found", via: :all, ' \
|
|
120
|
+
'constraints: ->(req) { !req.path.start_with?("/rails/", "/assets/") }',
|
|
121
|
+
:yellow
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def add_permittable_to_user
|
|
125
|
+
user_path = Rails.root.join("app/models/user.rb")
|
|
126
|
+
unless File.exist?(user_path)
|
|
127
|
+
say "Skipping User: app/models/user.rb not found.", :yellow
|
|
128
|
+
return
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
return if File.read(user_path).include?("RubyCms::Permittable")
|
|
132
|
+
|
|
133
|
+
inject_into_file user_path, after: /class User .*\n/ do
|
|
134
|
+
" include RubyCms::Permittable\n"
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def add_current_user_to_authentication
|
|
139
|
+
auth_path = Rails.root.join("app/controllers/concerns/authentication.rb")
|
|
140
|
+
return unless auth_path.exist?
|
|
141
|
+
return if File.read(auth_path).include?("def current_user")
|
|
142
|
+
|
|
143
|
+
gsub_file auth_path, " helper_method :authenticated?\n",
|
|
144
|
+
" helper_method :authenticated?, :current_user\n"
|
|
145
|
+
inject_into_file auth_path, after: " private\n" do
|
|
146
|
+
" def current_user\n Current.user\n end\n\n"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def add_visitor_error_capture
|
|
151
|
+
ac_path = Rails.root.join("app/controllers/application_controller.rb")
|
|
152
|
+
return unless ac_path.exist?
|
|
153
|
+
|
|
154
|
+
content = File.read(ac_path)
|
|
155
|
+
return if content.include?("RubyCms::VisitorErrorCapture")
|
|
156
|
+
|
|
157
|
+
to_inject = " include RubyCms::VisitorErrorCapture\n"
|
|
158
|
+
to_inject += " rescue_from StandardError, with: :handle_visitor_error\n" \
|
|
159
|
+
unless content.include?("rescue_from StandardError")
|
|
160
|
+
|
|
161
|
+
inject_into_file ac_path, after: /class ApplicationController.*\n/ do
|
|
162
|
+
to_inject
|
|
163
|
+
end
|
|
164
|
+
say "✓ Visitor error capture: Added to ApplicationController", :green
|
|
165
|
+
rescue StandardError => e
|
|
166
|
+
say "⚠ Visitor error capture: Could not add to ApplicationController: #{e.message}. " \
|
|
167
|
+
"Add manually: include RubyCms::VisitorErrorCapture and rescue_from StandardError, with: :handle_visitor_error",
|
|
168
|
+
:yellow
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def add_page_tracking_to_home_controller
|
|
172
|
+
home_path = Rails.root.join("app/controllers/home_controller.rb")
|
|
173
|
+
return unless home_path.exist?
|
|
174
|
+
|
|
175
|
+
content = File.read(home_path)
|
|
176
|
+
return if content.include?("RubyCms::PageTracking")
|
|
177
|
+
|
|
178
|
+
inject_into_file home_path, after: /class HomeController.*\n/ do
|
|
179
|
+
" include RubyCms::PageTracking\n"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
say "✓ Page tracking: Added RubyCms::PageTracking to HomeController", :green
|
|
183
|
+
rescue StandardError => e
|
|
184
|
+
say "⚠ Page tracking: Could not add to HomeController: #{e.message}. " \
|
|
185
|
+
"Add manually: include RubyCms::PageTracking",
|
|
186
|
+
:yellow
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def copy_fallback_css
|
|
190
|
+
src_dir = RubyCms::Engine.root.join("app/assets/stylesheets/ruby_cms")
|
|
191
|
+
dest_dir = Rails.root.join("app/assets/stylesheets/ruby_cms")
|
|
192
|
+
|
|
193
|
+
return unless src_dir.exist?
|
|
194
|
+
|
|
195
|
+
FileUtils.mkdir_p(dest_dir)
|
|
196
|
+
copy_admin_css(dest_dir)
|
|
197
|
+
# Don't copy component files - only the compiled admin.css is needed
|
|
198
|
+
# copy_components_css(src_dir, dest_dir)
|
|
199
|
+
say "✓ Task css/copy: Combined component CSS into " \
|
|
200
|
+
"app/assets/stylesheets/ruby_cms/admin.css", :green
|
|
201
|
+
rescue StandardError => e
|
|
202
|
+
say "⚠ Task css/copy: Could not copy CSS files: #{e.message}.", :yellow
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def create_admin_layout
|
|
206
|
+
layout_path = Rails.root.join("app/views/layouts/admin.html.erb")
|
|
207
|
+
return if File.exist?(layout_path)
|
|
208
|
+
|
|
209
|
+
template "admin.html.erb", layout_path.to_s
|
|
210
|
+
say "✓ Layout admin: Created app/views/layouts/admin.html.erb", :green
|
|
211
|
+
rescue StandardError => e
|
|
212
|
+
say "⚠ Layout admin: Could not create admin.html.erb: #{e.message}. " \
|
|
213
|
+
"Create it manually using the RubyCMS template.", :yellow
|
|
214
|
+
end
|
|
215
|
+
no_tasks do
|
|
216
|
+
def copy_admin_css(dest_dir)
|
|
217
|
+
admin_css_dest = dest_dir.join("admin.css")
|
|
218
|
+
RubyCms::Engine.compile_admin_css(admin_css_dest)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def copy_components_css(src_dir, dest_dir)
|
|
222
|
+
components_src_dir = src_dir.join("components")
|
|
223
|
+
components_dest_dir = dest_dir.join("components")
|
|
224
|
+
return unless components_src_dir.exist? && components_src_dir.directory?
|
|
225
|
+
|
|
226
|
+
FileUtils.mkdir_p(components_dest_dir)
|
|
227
|
+
Dir.glob(components_src_dir.join("*.css")).each do |src_file|
|
|
228
|
+
copy_component_css_file(src_file, components_dest_dir)
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def copy_component_css_file(src_file, dest_dir)
|
|
233
|
+
filename = File.basename(src_file)
|
|
234
|
+
dest_file = dest_dir.join(filename)
|
|
235
|
+
return if dest_file.exist? && File.mtime(src_file) <= File.mtime(dest_file)
|
|
236
|
+
|
|
237
|
+
FileUtils.cp(src_file, dest_file, preserve: true)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def install_ahoy
|
|
242
|
+
if ahoy_already_installed?
|
|
243
|
+
say "ℹ Task ahoy: Existing Ahoy setup detected (tables or migrations). Skipping ahoy:install.",
|
|
244
|
+
:cyan
|
|
245
|
+
configure_ahoy_server_side_only
|
|
246
|
+
return
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
say "ℹ Task ahoy: Installing Ahoy for visit/event tracking.", :cyan
|
|
250
|
+
run "bin/rails generate ahoy:install"
|
|
251
|
+
add_ahoy_security_fields_migration
|
|
252
|
+
configure_ahoy_server_side_only
|
|
253
|
+
say "✓ Task ahoy: Installed Ahoy (visits, events, tracking)", :green
|
|
254
|
+
rescue StandardError => e
|
|
255
|
+
say "⚠ Task ahoy: Could not install: #{e.message}. " \
|
|
256
|
+
"Run 'rails g ahoy:install' manually.",
|
|
257
|
+
:yellow
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def install_action_text
|
|
261
|
+
migrate_dir = Rails.root.join("db/migrate")
|
|
262
|
+
return unless migrate_dir.directory?
|
|
263
|
+
return if action_text_already_installed?(migrate_dir)
|
|
264
|
+
|
|
265
|
+
say "ℹ Task action_text: Installing Action Text for rich text/image content blocks.", :cyan
|
|
266
|
+
run "bin/rails action_text:install"
|
|
267
|
+
say "✓ Task action_text: Installed Action Text", :green
|
|
268
|
+
rescue StandardError => e
|
|
269
|
+
say "⚠ Task action_text: Could not install: #{e.message}. Rich text will be disabled.",
|
|
270
|
+
:yellow
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
no_tasks do
|
|
274
|
+
def ahoy_already_installed?
|
|
275
|
+
return true if ahoy_tables_exist?
|
|
276
|
+
|
|
277
|
+
migrate_dir = Rails.root.join("db/migrate")
|
|
278
|
+
return false unless migrate_dir.directory?
|
|
279
|
+
|
|
280
|
+
Dir.glob(migrate_dir.join("*.rb").to_s).any? do |f|
|
|
281
|
+
content = File.read(f)
|
|
282
|
+
content.include?("ahoy_visits") || content.include?("ahoy_events")
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def ahoy_tables_exist?
|
|
287
|
+
return false unless defined?(ActiveRecord::Base)
|
|
288
|
+
|
|
289
|
+
# Calling connection lazily establishes a connection when possible,
|
|
290
|
+
# so we can detect existing Ahoy tables even before AR reports connected?.
|
|
291
|
+
c = ActiveRecord::Base.connection
|
|
292
|
+
c.data_source_exists?("ahoy_visits") || c.data_source_exists?("ahoy_events")
|
|
293
|
+
rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError,
|
|
294
|
+
ActiveRecord::StatementInvalid
|
|
295
|
+
false
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def add_ahoy_security_fields_migration
|
|
299
|
+
run "bin/rails generate migration AddRubyCmsFieldsToAhoyEvents"
|
|
300
|
+
migration_file = Rails.root.glob("db/migrate/*add_ruby_cms*fields*.rb").max_by(&:basename)
|
|
301
|
+
return unless migration_file
|
|
302
|
+
|
|
303
|
+
migration_file = migration_file.to_s
|
|
304
|
+
content = <<~RUBY
|
|
305
|
+
class AddRubyCmsFieldsToAhoyEvents < ActiveRecord::Migration[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]
|
|
306
|
+
def change
|
|
307
|
+
return unless table_exists?(:ahoy_events)
|
|
308
|
+
|
|
309
|
+
add_column :ahoy_events, :page_name, :string unless column_exists?(:ahoy_events, :page_name)
|
|
310
|
+
add_column :ahoy_events, :ip_address, :string unless column_exists?(:ahoy_events, :ip_address)
|
|
311
|
+
add_column :ahoy_events, :request_path, :string unless column_exists?(:ahoy_events, :request_path)
|
|
312
|
+
add_column :ahoy_events, :user_agent, :text unless column_exists?(:ahoy_events, :user_agent)
|
|
313
|
+
add_column :ahoy_events, :description, :text unless column_exists?(:ahoy_events, :description)
|
|
314
|
+
|
|
315
|
+
add_index :ahoy_events, :page_name, if_not_exists: true
|
|
316
|
+
add_index :ahoy_events, :ip_address, if_not_exists: true
|
|
317
|
+
add_index :ahoy_events, :request_path, if_not_exists: true
|
|
318
|
+
add_index :ahoy_events, [:name, :page_name], if_not_exists: true
|
|
319
|
+
add_index :ahoy_events, [:name, :request_path], if_not_exists: true
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
RUBY
|
|
323
|
+
File.write(migration_file, content)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def configure_ahoy_server_side_only
|
|
327
|
+
ahoy_path = Rails.root.join("config/initializers/ahoy.rb")
|
|
328
|
+
content = if ahoy_path.exist?
|
|
329
|
+
File.read(ahoy_path)
|
|
330
|
+
else
|
|
331
|
+
<<~RUBY
|
|
332
|
+
# Configure Ahoy
|
|
333
|
+
RUBY
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# Ensure Ahoy is loaded before any references (fixes NameError when
|
|
337
|
+
# ahoy_matey loads after initializers)
|
|
338
|
+
content = %(require "ahoy_matey"\n\n#{content}) unless content.include?('require "ahoy_matey"')
|
|
339
|
+
|
|
340
|
+
# Ensure a default Ahoy store class exists.
|
|
341
|
+
unless content.match?(/class\s+Ahoy::Store\s*<\s*Ahoy::DatabaseStore/)
|
|
342
|
+
content += <<~RUBY
|
|
343
|
+
|
|
344
|
+
class Ahoy::Store < Ahoy::DatabaseStore
|
|
345
|
+
end
|
|
346
|
+
RUBY
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
if content.include?("Ahoy.api = false")
|
|
350
|
+
File.write(ahoy_path, content)
|
|
351
|
+
return
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
append = "\n\n# RubyCMS: server-side tracking only (no JavaScript)\nAhoy.api = false\nAhoy.geocode = false\n"
|
|
355
|
+
File.write(ahoy_path, "#{content}#{append}")
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def action_text_already_installed?(migrate_dir)
|
|
359
|
+
return true if active_storage_tables_exist? || action_text_tables_exist?
|
|
360
|
+
|
|
361
|
+
existing = Dir.glob(migrate_dir.join("*.rb").to_s).join("\n")
|
|
362
|
+
existing.include?("create_action_text_rich_texts") ||
|
|
363
|
+
existing.include?("create_active_storage_tables")
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def active_storage_tables_exist?
|
|
367
|
+
return false unless defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
|
|
368
|
+
|
|
369
|
+
c = ActiveRecord::Base.connection
|
|
370
|
+
c.data_source_exists?("active_storage_blobs") &&
|
|
371
|
+
c.data_source_exists?("active_storage_attachments")
|
|
372
|
+
rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError
|
|
373
|
+
false
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def action_text_tables_exist?
|
|
377
|
+
return false unless defined?(ActiveRecord::Base) && ActiveRecord::Base.connected?
|
|
378
|
+
|
|
379
|
+
ActiveRecord::Base.connection.data_source_exists?("action_text_rich_texts")
|
|
380
|
+
rescue ActiveRecord::ConnectionNotEstablished, ActiveRecord::NoDatabaseError
|
|
381
|
+
false
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
def install_tailwind
|
|
386
|
+
gemfile = Rails.root.join("Gemfile")
|
|
387
|
+
tailwind_css = detect_tailwind_entry_css_path
|
|
388
|
+
|
|
389
|
+
install_tailwind_if_needed(gemfile, tailwind_css)
|
|
390
|
+
configure_tailwind(tailwind_css)
|
|
391
|
+
rescue StandardError => e
|
|
392
|
+
say "⚠ Task tailwind: Could not install: #{e.message}. Add tailwindcss-rails manually.",
|
|
393
|
+
:yellow
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
no_tasks do
|
|
397
|
+
def detect_tailwind_entry_css_path
|
|
398
|
+
candidates = [
|
|
399
|
+
Rails.root.join("app/assets/tailwind/application.css"),
|
|
400
|
+
Rails.root.join("app/assets/stylesheets/application.tailwind.css"),
|
|
401
|
+
Rails.root.join("app/assets/stylesheets/tailwind.css")
|
|
402
|
+
]
|
|
403
|
+
candidates.find(&:exist?) || candidates.first
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def install_tailwind_if_needed(gemfile, tailwind_css)
|
|
407
|
+
return if File.exist?(tailwind_css)
|
|
408
|
+
|
|
409
|
+
if File.read(gemfile).include?("tailwindcss-rails")
|
|
410
|
+
say "ℹ Task tailwind: Tailwind CSS gem found; running tailwindcss:install.", :cyan
|
|
411
|
+
else
|
|
412
|
+
say "ℹ Task tailwind: Adding tailwindcss-rails for admin styling.", :cyan
|
|
413
|
+
run "bundle add tailwindcss-rails"
|
|
414
|
+
end
|
|
415
|
+
run "bin/rails tailwindcss:install"
|
|
416
|
+
say "✓ Task tailwind: Installed Tailwind CSS", :green
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
def configure_tailwind(tailwind_css)
|
|
420
|
+
add_ruby_cms_tailwind_source(tailwind_css)
|
|
421
|
+
add_ruby_cms_tailwind_content_paths
|
|
422
|
+
run "bin/rails tailwindcss:build" if File.exist?(tailwind_css)
|
|
423
|
+
# Importmap pins are provided by the engine via `ruby_cms/config/importmap.rb`.
|
|
424
|
+
add_importmap_pins
|
|
425
|
+
add_stimulus_registration
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def install_ruby_ui
|
|
430
|
+
gemfile = Rails.root.join("Gemfile")
|
|
431
|
+
gemfile_content = File.read(gemfile)
|
|
432
|
+
return if ruby_ui_in_gemfile?(gemfile_content)
|
|
433
|
+
|
|
434
|
+
add_ruby_ui_gem
|
|
435
|
+
rescue StandardError => e
|
|
436
|
+
say "⚠ Task ruby_ui: Could not add: #{e.message}. " \
|
|
437
|
+
"Run 'bundle add ruby_ui --group development --require false' manually.",
|
|
438
|
+
:yellow
|
|
439
|
+
nil
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def run_ruby_ui_install
|
|
443
|
+
gemfile = Rails.root.join("Gemfile")
|
|
444
|
+
gemfile_content = File.read(gemfile)
|
|
445
|
+
return unless ruby_ui_in_gemfile?(gemfile_content)
|
|
446
|
+
return if ruby_ui_already_installed?
|
|
447
|
+
|
|
448
|
+
install_ruby_ui_generator
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
no_tasks do
|
|
452
|
+
def ruby_ui_in_gemfile?(content)
|
|
453
|
+
content.include?("ruby_ui") || content.include?("rails_ui")
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def add_ruby_ui_gem
|
|
457
|
+
say "ℹ Task ruby_ui: Adding ruby_ui to Gemfile.", :cyan
|
|
458
|
+
run "bundle add ruby_ui --group development --require false"
|
|
459
|
+
run "bundle install"
|
|
460
|
+
say "✓ Task ruby_ui: Added ruby_ui to Gemfile", :green
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def ruby_ui_already_installed?
|
|
464
|
+
ruby_ui_initializer = Rails.root.join("config/initializers/ruby_ui.rb")
|
|
465
|
+
ruby_ui_base = Rails.root.join("app/components/ruby_ui/base.rb")
|
|
466
|
+
if File.exist?(ruby_ui_initializer) || File.exist?(ruby_ui_base)
|
|
467
|
+
say "ℹ Task ruby_ui:install: ruby_ui is already installed. Skipping.", :cyan
|
|
468
|
+
return true
|
|
469
|
+
end
|
|
470
|
+
false
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def install_ruby_ui_generator
|
|
474
|
+
say "ℹ Task ruby_ui:install: Running ruby_ui:install generator.", :cyan
|
|
475
|
+
try_ruby_ui_install
|
|
476
|
+
rescue StandardError => e
|
|
477
|
+
say "⚠ Task ruby_ui:install: Could not find generator: #{e.message}. " \
|
|
478
|
+
"Run 'rails g ruby_ui:install' manually.",
|
|
479
|
+
:yellow
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def try_ruby_ui_install
|
|
483
|
+
run "bin/rails generate ruby_ui:install"
|
|
484
|
+
say "✓ Task ruby_ui:install: Installed ruby_ui", :green
|
|
485
|
+
rescue StandardError
|
|
486
|
+
try_rails_ui_install
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def try_rails_ui_install
|
|
490
|
+
say "ℹ Task ruby_ui:install: Using rails_ui:install (ruby_ui alias).", :cyan
|
|
491
|
+
run "bin/rails generate rails_ui:install"
|
|
492
|
+
say "✓ Task ruby_ui:install: Installed rails_ui", :green
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def add_ruby_ui_to_application_helper
|
|
496
|
+
helper_path = Rails.root.join("app/helpers/application_helper.rb")
|
|
497
|
+
return unless File.exist?(helper_path)
|
|
498
|
+
|
|
499
|
+
content = File.read(helper_path)
|
|
500
|
+
return if ruby_ui_already_included?(content)
|
|
501
|
+
|
|
502
|
+
inject_ruby_ui_include(helper_path)
|
|
503
|
+
rescue StandardError => e
|
|
504
|
+
say "⚠ Task ruby_ui/helper: Could not add RubyUI: #{e.message}. " \
|
|
505
|
+
"Add 'include RubyUI' manually.",
|
|
506
|
+
:yellow
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def ruby_ui_already_included?(content)
|
|
510
|
+
content.include?("include RubyUI") || content.include?("include RubyUi")
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def inject_ruby_ui_include(helper_path)
|
|
514
|
+
say "✓ Task ruby_ui/helper: Added include RubyUI to ApplicationHelper.", :green
|
|
515
|
+
inject_into_file helper_path.to_s, after: /module ApplicationHelper\n/ do
|
|
516
|
+
" include RubyUI\n"
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def generate_ruby_ui_components
|
|
521
|
+
# Skip component generation - page builder removed.
|
|
522
|
+
# Generate manually: rails g ruby_ui:component ComponentName
|
|
523
|
+
say "ℹ Task ruby_ui/components: Skipping automatic component generation " \
|
|
524
|
+
"(page builder removed). " \
|
|
525
|
+
"Generate components manually if needed: rails g ruby_ui:component ComponentName",
|
|
526
|
+
:cyan
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# Directories to skip when scanning for page templates
|
|
531
|
+
SKIP_VIEW_DIRS = %w[layouts shared mailers components admin].freeze
|
|
532
|
+
|
|
533
|
+
# NOTE: Rails generators are Thor groups; private methods can still be
|
|
534
|
+
# treated as "tasks" unless wrapped in `no_tasks`.
|
|
535
|
+
no_tasks do
|
|
536
|
+
def detect_page_templates
|
|
537
|
+
views_dir = Rails.root.join("app/views")
|
|
538
|
+
return {} unless Dir.exist?(views_dir)
|
|
539
|
+
|
|
540
|
+
pages = {}
|
|
541
|
+
views_base = views_dir.to_s
|
|
542
|
+
scan_for_templates(views_dir, pages, views_base)
|
|
543
|
+
log_detected_pages(pages) if pages.any?
|
|
544
|
+
pages
|
|
545
|
+
rescue StandardError => e
|
|
546
|
+
say "⚠ Task pages: Could not scan for page templates: #{e.message}.", :yellow
|
|
547
|
+
{}
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def scan_for_templates(dir_path, pages, views_base, relative_path="")
|
|
551
|
+
# Find all template files in this directory
|
|
552
|
+
Dir.glob(File.join(dir_path, "*.{html.erb,html.haml,html.slim}")).each do |template_file|
|
|
553
|
+
base_name = File.basename(template_file, ".*")
|
|
554
|
+
base_name = File.basename(base_name, ".*") # Remove .html extension
|
|
555
|
+
|
|
556
|
+
# Skip partials
|
|
557
|
+
next if base_name.start_with?("_")
|
|
558
|
+
# Skip admin pages
|
|
559
|
+
next if relative_path.start_with?("admin") || relative_path == "admin"
|
|
560
|
+
|
|
561
|
+
# Build template path relative to app/views
|
|
562
|
+
if relative_path.empty?
|
|
563
|
+
template_path = base_name
|
|
564
|
+
page_key = base_name
|
|
565
|
+
elsif base_name == "index"
|
|
566
|
+
page_key = relative_path.split("/").last
|
|
567
|
+
template_path = "#{relative_path}/index"
|
|
568
|
+
# pages/index.html.erb -> "pages" => "pages/index"
|
|
569
|
+
else
|
|
570
|
+
# pages/home.html.erb -> "home" => "pages/home"
|
|
571
|
+
page_key = base_name
|
|
572
|
+
template_path = "#{relative_path}/#{base_name}"
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
pages[page_key] = template_path
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
# Recursively scan subdirectories (skip common non-page dirs, limit depth)
|
|
579
|
+
Dir.glob(File.join(dir_path, "*")).each do |path|
|
|
580
|
+
next unless File.directory?(path)
|
|
581
|
+
|
|
582
|
+
dir_name = File.basename(path)
|
|
583
|
+
# Skip admin directories and common non-page directories
|
|
584
|
+
next if SKIP_VIEW_DIRS.include?(dir_name)
|
|
585
|
+
# Skip if we're already in an admin path
|
|
586
|
+
next if relative_path.start_with?("admin")
|
|
587
|
+
|
|
588
|
+
# Limit depth to 2 levels (e.g., app/views/pages/home is OK, but not deeper)
|
|
589
|
+
depth = relative_path.empty? ? 1 : relative_path.split("/").length + 1
|
|
590
|
+
next if depth > 2
|
|
591
|
+
|
|
592
|
+
new_relative_path = relative_path.empty? ? dir_name : "#{relative_path}/#{dir_name}"
|
|
593
|
+
scan_for_templates(path, pages, views_base, new_relative_path)
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
597
|
+
|
|
598
|
+
def log_detected_pages(pages)
|
|
599
|
+
say "✓ Task pages: Detected #{pages.size} page template(s): " \
|
|
600
|
+
"#{pages.keys.join(', ')}", :green
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def add_importmap_pins
|
|
604
|
+
importmap_path = Rails.root.join("config/importmap.rb")
|
|
605
|
+
return unless File.exist?(importmap_path)
|
|
606
|
+
|
|
607
|
+
content = File.read(importmap_path)
|
|
608
|
+
gem_js_path = calculate_gem_js_path
|
|
609
|
+
return if importmap_already_configured?(content, gem_js_path)
|
|
610
|
+
|
|
611
|
+
inject_importmap_pins(importmap_path, gem_js_path)
|
|
612
|
+
rescue StandardError => e
|
|
613
|
+
say "⚠ Task importmap: Could not add pins: #{e.message}. " \
|
|
614
|
+
"Add manually to config/importmap.rb.",
|
|
615
|
+
:yellow
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def calculate_gem_js_path
|
|
619
|
+
RubyCms::Engine.root.join("app/javascript").relative_path_from(Rails.root).to_s
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def importmap_already_configured?(content, gem_js_path)
|
|
623
|
+
pin_pattern = %(pin_all_from "#{gem_js_path}/controllers", under: "controllers")
|
|
624
|
+
content.include?("RubyCMS Stimulus controllers") || content.include?(pin_pattern)
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
def inject_importmap_pins(importmap_path, gem_js_path)
|
|
628
|
+
pin_line = %(pin_all_from "#{gem_js_path}/controllers", under: "controllers")
|
|
629
|
+
alias_line = %(pin "ruby_cms", to: "controllers/ruby_cms/index.js", preload: true)
|
|
630
|
+
inject_into_file importmap_path.to_s, before: /^end/ do
|
|
631
|
+
"\n # RubyCMS Stimulus controllers\n #{pin_line}\n #{alias_line}\n"
|
|
632
|
+
end
|
|
633
|
+
say "✓ Task importmap: Added RubyCMS controllers to importmap.rb.", :green
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def add_stimulus_registration
|
|
637
|
+
controllers_app_path = Rails.root.join("app/javascript/controllers/application.js")
|
|
638
|
+
return handle_missing_stimulus_file unless File.exist?(controllers_app_path)
|
|
639
|
+
|
|
640
|
+
content = File.read(controllers_app_path)
|
|
641
|
+
unless stimulus_already_exposed?(content)
|
|
642
|
+
expose_stimulus_application(controllers_app_path,
|
|
643
|
+
content)
|
|
644
|
+
end
|
|
645
|
+
import_rubycms_controllers(controllers_app_path, content)
|
|
646
|
+
cleanup_old_registration_code
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def handle_missing_stimulus_file
|
|
650
|
+
say "⚠ Task stimulus: Could not find app/javascript/controllers/application.js. " \
|
|
651
|
+
"RubyCMS controllers may need manual setup.",
|
|
652
|
+
:yellow
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def stimulus_already_exposed?(content)
|
|
656
|
+
if content.include?("window.Stimulus") || content.include?("window.application")
|
|
657
|
+
say "ℹ Task stimulus: Stimulus application is already exposed on window.", :cyan
|
|
658
|
+
return true
|
|
659
|
+
end
|
|
660
|
+
false
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def expose_stimulus_application(controllers_app_path, content)
|
|
664
|
+
return unless stimulus_application_startable?(content)
|
|
665
|
+
|
|
666
|
+
if stimulus_const_assignment?(content)
|
|
667
|
+
add_stimulus_const_pattern(controllers_app_path)
|
|
668
|
+
elsif stimulus_var_assignment?(content)
|
|
669
|
+
add_stimulus_var_pattern(controllers_app_path)
|
|
670
|
+
else
|
|
671
|
+
warn_manual_stimulus_exposure
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def stimulus_application_startable?(content)
|
|
676
|
+
content.include?("const application") && content.include?("Application.start")
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
def stimulus_const_assignment?(content)
|
|
680
|
+
content.match?(/const\s+application\s*=\s*Application\.start\(\)/)
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
def stimulus_var_assignment?(content)
|
|
684
|
+
content.match?(/application\s*=\s*Application\.start\(\)/)
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
def warn_manual_stimulus_exposure
|
|
688
|
+
say "⚠ Task stimulus: Could not automatically expose. " \
|
|
689
|
+
"Manually add 'window.Stimulus = application'.",
|
|
690
|
+
:yellow
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def add_stimulus_const_pattern(controllers_app_path)
|
|
694
|
+
gsub_file controllers_app_path.to_s,
|
|
695
|
+
/const\s+application\s*=\s*Application\.start\(\)/,
|
|
696
|
+
"const application = Application.start()\nwindow.Stimulus = application"
|
|
697
|
+
say_stimulus_added
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
def add_stimulus_var_pattern(controllers_app_path)
|
|
701
|
+
gsub_file controllers_app_path.to_s,
|
|
702
|
+
/application\s*=\s*Application\.start\(\)/,
|
|
703
|
+
"application = Application.start()\nwindow.Stimulus = application"
|
|
704
|
+
say_stimulus_added
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def say_stimulus_added
|
|
708
|
+
say "✓ Task stimulus: Added window.Stimulus = application to " \
|
|
709
|
+
"controllers/application.js.",
|
|
710
|
+
:green
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
714
|
+
def import_rubycms_controllers(controllers_app_path, content)
|
|
715
|
+
# Re-read content in case it was modified by expose_stimulus_application
|
|
716
|
+
content = File.read(controllers_app_path) if File.exist?(controllers_app_path)
|
|
717
|
+
|
|
718
|
+
return if content.include?('import "ruby_cms"') || content.include?("import 'ruby_cms'")
|
|
719
|
+
return if content.include?("registerRubyCmsControllers")
|
|
720
|
+
|
|
721
|
+
# Try to add after the Stimulus import (most common pattern)
|
|
722
|
+
stimulus_import_pattern = %r{import\s+.*@hotwired/stimulus.*$}
|
|
723
|
+
if content.match?(stimulus_import_pattern)
|
|
724
|
+
inject_into_file controllers_app_path.to_s,
|
|
725
|
+
after: stimulus_import_pattern,
|
|
726
|
+
verbose: false do
|
|
727
|
+
"\nimport \"ruby_cms\""
|
|
728
|
+
end
|
|
729
|
+
say "✓ Task stimulus: Added RubyCMS controllers import.", :green
|
|
730
|
+
return
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
# Try to add after any import statement
|
|
734
|
+
first_import_pattern = /^import\s+.*$/m
|
|
735
|
+
if content.match?(first_import_pattern)
|
|
736
|
+
inject_into_file controllers_app_path.to_s,
|
|
737
|
+
after: first_import_pattern,
|
|
738
|
+
verbose: false do
|
|
739
|
+
"\nimport \"ruby_cms\""
|
|
740
|
+
end
|
|
741
|
+
say "✓ Task stimulus: Added RubyCMS controllers import.", :green
|
|
742
|
+
return
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# Add at the very top if no imports found
|
|
746
|
+
prepend_to_file controllers_app_path.to_s, "import \"ruby_cms\"\n"
|
|
747
|
+
say "✓ Task stimulus: Added RubyCMS controllers import.", :green
|
|
748
|
+
rescue StandardError => e
|
|
749
|
+
say "⚠ Task stimulus: Could not add RubyCMS import: #{e.message}. " \
|
|
750
|
+
"Add 'import \"ruby_cms\"' manually to controllers/application.js.",
|
|
751
|
+
:yellow
|
|
752
|
+
end
|
|
753
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
754
|
+
|
|
755
|
+
def cleanup_old_registration_code
|
|
756
|
+
js_files = find_js_files_to_check
|
|
757
|
+
js_files.each {|js_file| cleanup_file_registration(js_file) }
|
|
758
|
+
rescue StandardError
|
|
759
|
+
# Silent fail - not critical
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
def find_js_files_to_check
|
|
763
|
+
[
|
|
764
|
+
Rails.root.join("app/javascript/application.js"),
|
|
765
|
+
Rails.root.join("app/javascript/index.js"),
|
|
766
|
+
Rails.root.join("app/javascript/entrypoints/application.js")
|
|
767
|
+
].select(&:exist?)
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def cleanup_file_registration(js_file)
|
|
771
|
+
content = File.read(js_file)
|
|
772
|
+
return unless needs_cleanup?(content)
|
|
773
|
+
|
|
774
|
+
new_content = remove_registration_lines(content)
|
|
775
|
+
return if new_content == content
|
|
776
|
+
|
|
777
|
+
File.write(js_file, new_content)
|
|
778
|
+
say "✓ Task stimulus: Removed old manual registration from #{js_file.basename}. " \
|
|
779
|
+
"Auto-registration handles this now.",
|
|
780
|
+
:green
|
|
781
|
+
end
|
|
782
|
+
|
|
783
|
+
def needs_cleanup?(content)
|
|
784
|
+
content.include?("registerRubyCmsControllers(application)") &&
|
|
785
|
+
content.exclude?("if (typeof application !== \"undefined\")")
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
def remove_registration_lines(content)
|
|
789
|
+
lines = content.split("\n")
|
|
790
|
+
lines.reject do |line|
|
|
791
|
+
line.strip.include?("registerRubyCmsControllers(application)") ||
|
|
792
|
+
(line.strip.start_with?("import") && line.include?("registerRubyCmsControllers") &&
|
|
793
|
+
line.include?("ruby_cms"))
|
|
794
|
+
end.join("\n")
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
# Helper: add @source for RubyCMS views/components so Tailwind finds utility classes.
|
|
798
|
+
# Not a generator task.
|
|
799
|
+
def add_ruby_cms_tailwind_source(tailwind_css_path)
|
|
800
|
+
return unless tailwind_css_path.to_s.present? && File.exist?(tailwind_css_path)
|
|
801
|
+
|
|
802
|
+
content = File.read(tailwind_css_path)
|
|
803
|
+
gem_source_lines = build_gem_source_lines(tailwind_css_path)
|
|
804
|
+
return if gem_source_lines.all? {|line| content.include?(line) }
|
|
805
|
+
|
|
806
|
+
inject_tailwind_source(tailwind_css_path, content, gem_source_lines)
|
|
807
|
+
rescue StandardError => e
|
|
808
|
+
say "⚠ Task tailwind/source: Could not add @source: #{e.message}. Add manually.", :yellow
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
# Tailwind v3 support (tailwind.config.js content array)
|
|
812
|
+
def add_ruby_cms_tailwind_content_paths # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
813
|
+
config_path = Rails.root.join("config/tailwind.config.js")
|
|
814
|
+
return unless File.exist?(config_path)
|
|
815
|
+
|
|
816
|
+
content = File.read(config_path)
|
|
817
|
+
patterns = ruby_cms_tailwind_content_patterns
|
|
818
|
+
return if patterns.all? {|p| content.include?(p) }
|
|
819
|
+
|
|
820
|
+
inject = "#{patterns.map {|p| " \"#{p}\"," }.join("\n")}\n"
|
|
821
|
+
|
|
822
|
+
# Insert inside `content: [` if present; otherwise no-op.
|
|
823
|
+
inserted = false
|
|
824
|
+
if content.match?(/content:\s*\[/)
|
|
825
|
+
gsub_file config_path.to_s, /content:\s*\[\s*\n/ do |match|
|
|
826
|
+
inserted = true
|
|
827
|
+
"#{match}#{inject}"
|
|
828
|
+
end
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
return unless inserted
|
|
832
|
+
|
|
833
|
+
say "✓ Task tailwind/content: Added RubyCMS paths to config/tailwind.config.js.", :green
|
|
834
|
+
rescue StandardError => e
|
|
835
|
+
say "⚠ Task tailwind/content: Could not update tailwind.config.js: #{e.message}.", :yellow
|
|
836
|
+
end
|
|
837
|
+
|
|
838
|
+
def ruby_cms_tailwind_content_patterns
|
|
839
|
+
views = RubyCms::Engine.root.join("app/views").relative_path_from(Rails.root).to_s
|
|
840
|
+
components = RubyCms::Engine.root.join("app/components").relative_path_from(Rails.root).to_s
|
|
841
|
+
[
|
|
842
|
+
"#{views}/**/*.erb",
|
|
843
|
+
"#{components}/**/*.rb"
|
|
844
|
+
]
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
def build_gem_source_lines(tailwind_css_path)
|
|
848
|
+
css_dir = Pathname.new(tailwind_css_path).dirname
|
|
849
|
+
gem_views = path_relative_to_css_or_absolute(RubyCms::Engine.root.join("app/views"),
|
|
850
|
+
css_dir)
|
|
851
|
+
gem_components = path_relative_to_css_or_absolute(
|
|
852
|
+
RubyCms::Engine.root.join("app/components"), css_dir
|
|
853
|
+
)
|
|
854
|
+
[
|
|
855
|
+
%(@source "#{gem_views}/**/*.erb";),
|
|
856
|
+
%(@source "#{gem_components}/**/*.rb";)
|
|
857
|
+
]
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def path_relative_to_css_or_absolute(target_path, css_dir)
|
|
861
|
+
Pathname.new(target_path).relative_path_from(css_dir).to_s
|
|
862
|
+
rescue ArgumentError
|
|
863
|
+
# Different mount/volume: fall back to absolute path.
|
|
864
|
+
Pathname.new(target_path).to_s
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
def inject_tailwind_source(tailwind_css_path, content, gem_source_lines)
|
|
868
|
+
to_inject = build_tailwind_source_injection(gem_source_lines)
|
|
869
|
+
inserted = try_insert_after_patterns?(tailwind_css_path, content, to_inject)
|
|
870
|
+
inject_at_start(tailwind_css_path, to_inject) unless inserted
|
|
871
|
+
say "✓ Task tailwind/source: Added @source for RubyCMS views/components to " \
|
|
872
|
+
"tailwind/application.css.",
|
|
873
|
+
:green
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
def build_tailwind_source_injection(gem_source_lines)
|
|
877
|
+
to_inject = +"\n/* Include RubyCMS views/components so Tailwind finds utility classes. */\n"
|
|
878
|
+
Array(gem_source_lines).each {|line| to_inject << line << "\n" }
|
|
879
|
+
to_inject << "\n"
|
|
880
|
+
to_inject
|
|
881
|
+
end
|
|
882
|
+
|
|
883
|
+
def try_insert_after_patterns?(tailwind_css_path, content, to_inject)
|
|
884
|
+
patterns = [
|
|
885
|
+
%(@import "tailwindcss";\n),
|
|
886
|
+
%(@import "tailwindcss";),
|
|
887
|
+
%(@import "tailwindcss"\n),
|
|
888
|
+
%(@import "tailwindcss")
|
|
889
|
+
]
|
|
890
|
+
patterns.each do |after_pattern|
|
|
891
|
+
next unless content.include?(after_pattern)
|
|
892
|
+
|
|
893
|
+
inject_into_file tailwind_css_path.to_s, after: after_pattern do
|
|
894
|
+
to_inject
|
|
895
|
+
end
|
|
896
|
+
return true
|
|
897
|
+
end
|
|
898
|
+
false
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
def inject_at_start(tailwind_css_path, to_inject)
|
|
902
|
+
inject_into_file tailwind_css_path.to_s, after: /\A/ do
|
|
903
|
+
to_inject
|
|
904
|
+
end
|
|
905
|
+
end
|
|
906
|
+
end
|
|
907
|
+
|
|
908
|
+
def run_migrate
|
|
909
|
+
say "ℹ Task db:migrate: Running db:migrate.", :cyan
|
|
910
|
+
success = run("bin/rails db:migrate")
|
|
911
|
+
raise "db:migrate failed" unless success
|
|
912
|
+
|
|
913
|
+
say "✓ Task db:migrate: Completed", :green
|
|
914
|
+
rescue StandardError => e
|
|
915
|
+
say "⚠ Task db:migrate: Failed: #{e.message}. Run rails db:create db:migrate if needed.",
|
|
916
|
+
:yellow
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
def run_seed_permissions
|
|
920
|
+
say "ℹ Task permissions: Seeding RubyCMS permissions.", :cyan
|
|
921
|
+
success = seed_permissions_via_open3
|
|
922
|
+
say_seed_permissions_outcome(success)
|
|
923
|
+
rescue StandardError => e
|
|
924
|
+
say_seed_permissions_error(e)
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
def run_setup_admin
|
|
928
|
+
return if skip_setup_admin_due_to_existing_admin?
|
|
929
|
+
return unless setup_admin_tty?
|
|
930
|
+
|
|
931
|
+
run_setup_admin_task
|
|
932
|
+
rescue StandardError => e
|
|
933
|
+
say_setup_admin_error(e)
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
no_tasks do
|
|
937
|
+
def seed_permissions_via_open3
|
|
938
|
+
require "open3"
|
|
939
|
+
|
|
940
|
+
_stdin, stdout, stderr, wait_thr = open3_seed_permissions_process
|
|
941
|
+
stderr_thread = stream_seed_permissions_stderr(stderr)
|
|
942
|
+
stdout_thread = stream_seed_permissions_stdout(stdout)
|
|
943
|
+
|
|
944
|
+
success = wait_thr.value.success?
|
|
945
|
+
stderr_thread.join
|
|
946
|
+
stdout_thread.join
|
|
947
|
+
close_seed_permissions_streams(stdout, stderr)
|
|
948
|
+
success
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
def open3_seed_permissions_process
|
|
952
|
+
Open3.popen3(*seed_permissions_command, chdir: Rails.root.to_s)
|
|
953
|
+
end
|
|
954
|
+
|
|
955
|
+
def seed_permissions_command
|
|
956
|
+
# Use argv form to avoid invoking a shell.
|
|
957
|
+
%w[bin/rails ruby_cms:seed_permissions ruby_cms:import_initializer_settings]
|
|
958
|
+
end
|
|
959
|
+
|
|
960
|
+
def stream_seed_permissions_stderr(stderr)
|
|
961
|
+
Thread.new do
|
|
962
|
+
stderr.each_line do |line|
|
|
963
|
+
$stderr.print line unless line.include?("already initialized constant")
|
|
964
|
+
end
|
|
965
|
+
end
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
def stream_seed_permissions_stdout(stdout)
|
|
969
|
+
Thread.new do
|
|
970
|
+
stdout.each_line {|line| $stdout.print line }
|
|
971
|
+
end
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
def close_seed_permissions_streams(stdout, stderr)
|
|
975
|
+
stdout.close
|
|
976
|
+
stderr.close
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
def say_seed_permissions_outcome(success)
|
|
980
|
+
if success
|
|
981
|
+
say "✓ Task permissions: Seeded RubyCMS permissions", :green
|
|
982
|
+
else
|
|
983
|
+
say "⚠ Task permissions: Could not seed. " \
|
|
984
|
+
"Run 'rails ruby_cms:seed_permissions' manually.",
|
|
985
|
+
:yellow
|
|
986
|
+
end
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
def say_seed_permissions_error(error)
|
|
990
|
+
say "⚠ Task permissions: Could not seed: #{error.message}. " \
|
|
991
|
+
"Run 'rails ruby_cms:seed_permissions' manually.",
|
|
992
|
+
:yellow
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
def skip_setup_admin_due_to_existing_admin?
|
|
996
|
+
return false unless user_with_admin_permissions_exists?
|
|
997
|
+
|
|
998
|
+
say "ℹ Task setup_admin: User with admin permissions already exists. " \
|
|
999
|
+
"Skipping setup_admin.",
|
|
1000
|
+
:cyan
|
|
1001
|
+
true
|
|
1002
|
+
end
|
|
1003
|
+
|
|
1004
|
+
def setup_admin_tty?
|
|
1005
|
+
return true if $stdin.tty?
|
|
1006
|
+
|
|
1007
|
+
say "ℹ Task setup_admin: Skipping interactive setup (non-TTY). " \
|
|
1008
|
+
"Run: rails ruby_cms:setup_admin",
|
|
1009
|
+
:yellow
|
|
1010
|
+
false
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
def run_setup_admin_task
|
|
1014
|
+
say "ℹ Task setup_admin: Running setup_admin (create or pick first admin).", :cyan
|
|
1015
|
+
run "bin/rails ruby_cms:setup_admin"
|
|
1016
|
+
say "✓ Task setup_admin: Completed", :green
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
def say_setup_admin_error(error)
|
|
1020
|
+
say "⚠ Task setup_admin: Failed or skipped: #{error.message}. " \
|
|
1021
|
+
"Run: rails ruby_cms:setup_admin",
|
|
1022
|
+
:yellow
|
|
1023
|
+
end
|
|
1024
|
+
|
|
1025
|
+
def user_with_admin_permissions_exists? # rubocop:disable Metrics/MethodLength
|
|
1026
|
+
return false unless defined?(::User)
|
|
1027
|
+
|
|
1028
|
+
begin
|
|
1029
|
+
# Ensure permissions exist first
|
|
1030
|
+
RubyCms::Permission.ensure_defaults!
|
|
1031
|
+
|
|
1032
|
+
# Check if any user has ALL required admin permissions
|
|
1033
|
+
required_keys = %w[
|
|
1034
|
+
manage_admin
|
|
1035
|
+
manage_permissions
|
|
1036
|
+
manage_content_blocks
|
|
1037
|
+
manage_visitor_errors
|
|
1038
|
+
]
|
|
1039
|
+
required_permission_ids = RubyCms::Permission.where(key: required_keys).pluck(:id)
|
|
1040
|
+
return false if required_permission_ids.size != required_keys.size
|
|
1041
|
+
|
|
1042
|
+
# Find users who have all required permissions
|
|
1043
|
+
user_ids_with_all_perms = RubyCms::UserPermission
|
|
1044
|
+
.where(permission_id: required_permission_ids)
|
|
1045
|
+
.group(:user_id)
|
|
1046
|
+
.having("COUNT(DISTINCT permission_id) = ?", required_keys.size)
|
|
1047
|
+
.pluck(:user_id)
|
|
1048
|
+
|
|
1049
|
+
user_ids_with_all_perms.any?
|
|
1050
|
+
rescue StandardError
|
|
1051
|
+
# If there's an error (e.g., tables don't exist yet), assume no admin exists
|
|
1052
|
+
false
|
|
1053
|
+
end
|
|
1054
|
+
end
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
def show_next_steps
|
|
1058
|
+
say NEXT_STEPS_MESSAGE, :green
|
|
1059
|
+
end
|
|
1060
|
+
end
|
|
1061
|
+
end
|
|
1062
|
+
end
|