Class: LcpRuby::Generators::InstallGenerator
- Inherits:
-
Rails::Generators::Base
- Object
- Rails::Generators::Base
- LcpRuby::Generators::InstallGenerator
- Includes:
- FormatSupport
- Defined in:
- lib/generators/lcp_ruby/install_generator.rb
Constant Summary collapse
- SAMPLE_HEADER =
Header injected into each sample file (DSL + YAML both use ‘#` for comments, so one literal works for both formats). DslToYaml strips in-source comments during conversion, so we prepend after copy via `prepend_sample_header!` rather than embedding in the template.
<<~HEADER # DELETE ME — sample scaffold for first-day discovery. # `lcp_ruby:install` (and `lcp new` without `--skip-sample`) seeds # this `item` entity so a fresh install has something to click at # /items. Once you've added your own entities via # `rails g lcp_ruby:entity`, delete all four sample files: # config/lcp_ruby/models/item.{rb,yml} # config/lcp_ruby/presenters/items.{rb,yml} # config/lcp_ruby/permissions/item.yml # config/lcp_ruby/views/items.{rb,yml} # ...and remove `- view_group: items` from config/lcp_ruby/menu.yml. HEADER
- SPROCKETS_MANIFEST_LINKS =
Sprockets manifest entries required for LCP’s JS + CSS to be served by ‘javascript_include_tag` / `stylesheet_link_tag`. Used by `install_asset_pipeline_compat` (this generator) and mirrored in the engine’s ‘check_asset_pipeline_compat!` error message — keep the two in sync.
[ "//= link lcp_ruby/application.js", "//= link lcp_ruby/application.css", "//= link lcp_ruby/tom-select.css", "//= link lcp_ruby/tom-select.complete.min.js" ].freeze
Constants included from FormatSupport
Instance Method Summary collapse
- #add_autoloader_ignore ⇒ Object
- #add_current_user_helper ⇒ Object
- #add_lcp_ruby_require ⇒ Object
-
#configure_skip_asset_pipeline ⇒ Object
When the Sprockets compatibility shim is skipped, the engine’s boot check would otherwise raise LcpRuby::AssetPipelineError on the next non-generator boot.
-
#create_default_menu ⇒ Object
Ships a ‘config/lcp_ruby/menu.yml` with the auto-populated `top_menu:` slot plus a fully-commented user-menu block.
- #create_default_permissions ⇒ Object
- #create_directory_structure ⇒ Object
- #create_initializer ⇒ Object
- #create_sample_model ⇒ Object
- #create_sample_permissions ⇒ Object
- #create_sample_presenter ⇒ Object
- #create_sample_view_group ⇒ Object
-
#install_active_storage ⇒ Object
Active Storage backs the ‘attachment` field type and `rich_text` content, both of which are core to LCP.
-
#install_asset_pipeline_compat ⇒ Object
Sprockets compatibility shim (audit #16).
- #mount_engine ⇒ Object
-
#setup_agent_affordances ⇒ Object
Agent affordances: copies the lcp-* skills and writes CLAUDE.md/AGENTS.md so AI agents (Claude Code, Codex, …) have orientation + authoring skills.
- #show_post_install_message ⇒ Object
Methods included from FormatSupport
Instance Method Details
#add_autoloader_ignore ⇒ Object
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 75 def add_autoloader_ignore app_file = "config/application.rb" full_path = File.join(destination_root, app_file) return unless File.exist?(full_path) content = File.read(full_path) return if content.include?("ignore_lcp_services") inject_into_file app_file, before: /^ end\nend\s*\z/ do <<~RUBY.indent(4) initializer "\#{Rails.application.class.module_parent_name.underscore}.ignore_lcp_services", before: :set_autoload_paths do %w[condition_services lcp_services actions event_handlers renderers].each do |dir| path = Rails.root.join("app", dir) Rails.autoloaders.main.ignore(path) if path.directory? end end RUBY end end |
#add_current_user_helper ⇒ Object
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 321 def add_current_user_helper controller_path = "app/controllers/application_controller.rb" full_path = File.join(destination_root, controller_path) return unless File.exist?(full_path) return if File.read(full_path).include?("current_user") inject_into_class controller_path, "ApplicationController" do <<~RUBY.indent(2) # TODO: Replace with real authentication # Run: rails generate lcp_ruby:install_auth def current_user @current_user ||= OpenStruct.new(id: 1, lcp_role: ["admin"], name: "Admin User") end helper_method :current_user RUBY end end |
#add_lcp_ruby_require ⇒ Object
64 65 66 67 68 69 70 71 72 73 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 64 def add_lcp_ruby_require app_file = "config/application.rb" full_path = File.join(destination_root, app_file) return unless File.exist?(full_path) return if File.read(full_path).include?('require "lcp_ruby"') inject_into_file app_file, after: /Bundler\.require\(\*Rails\.groups\)\n/ do "require \"lcp_ruby\"\n" end end |
#configure_skip_asset_pipeline ⇒ Object
When the Sprockets compatibility shim is skipped, the engine’s boot check would otherwise raise LcpRuby::AssetPipelineError on the next non-generator boot. Write the matching opt-out so the documented ‘–skip-asset-pipeline` flag yields a bootable app.
MUST stay a separate step from ‘create_initializer` (which early-returns when the initializer already exists): on a re-run or install over a pre-existing initializer, the opt-out has to be applied regardless of whether the file was freshly created — otherwise the flag silently no-ops and the app won’t boot. Idempotent: skips when the line is already present.
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 280 def configure_skip_asset_pipeline return unless [:skip_asset_pipeline] initializer_path = "config/initializers/lcp_ruby.rb" full_path = File.join(destination_root, initializer_path) unless File.exist?(full_path) say_status :skip, "skip_asset_pipeline_check (no #{initializer_path})", :yellow return end content = File.read(full_path) return if content.include?("skip_asset_pipeline_check") # Match the configure block (both `do |x|` and brace `{ |x|` forms, the # latter is what the engine's AssetPipelineError message suggests) and # CAPTURE its block-variable name so the injected line targets the host's # actual var (`config`, `c`, …) — a hardcoded `config.` would NameError # at boot on a `|c|` initializer. Not anchored to a trailing newline, so # a `do |config| # comment` line still matches. match = content.match(/LcpRuby\.configure\s*(?:do|\{)\s*\|\s*(\w+)\s*\|/) unless match say_status :warn, "could not anchor skip_asset_pipeline_check in #{initializer_path}; " \ "add `config.skip_asset_pipeline_check = true` inside the LcpRuby.configure block manually", :yellow return end var = match[1] optout = <<~RUBY.indent(2) # --skip-asset-pipeline was passed: the Sprockets compatibility shim # is NOT installed, so the boot-time asset-pipeline check is opted # out too (otherwise the engine raises LcpRuby::AssetPipelineError). # Only correct if you've wired your own JS bundler (esbuild, # importmap with a custom pin map, jsbundling, etc.). #{var}.skip_asset_pipeline_check = true RUBY inject_into_file initializer_path, "\n#{optout}", after: match[0] end |
#create_default_menu ⇒ Object
Ships a ‘config/lcp_ruby/menu.yml` with the auto-populated `top_menu:` slot plus a fully-commented user-menu block. The block is opt-in: configurators uncomment after running `rails generate lcp_ruby:install_auth` (which wires Devise and the engine routes the block references). Without auth, the comments document the shape so the configurator discovers the capability when they need it.
169 170 171 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 169 def template "menu.yml.tt", "config/lcp_ruby/menu.yml" end |
#create_default_permissions ⇒ Object
158 159 160 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 158 def template "default_permissions.yml", "config/lcp_ruby/permissions/_default.yml" end |
#create_directory_structure ⇒ Object
118 119 120 121 122 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 118 def create_directory_structure %w[models presenters permissions views].each do |dir| empty_directory "config/lcp_ruby/#{dir}" end end |
#create_initializer ⇒ Object
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 173 def create_initializer initializer_path = "config/initializers/lcp_ruby.rb" full_path = File.join(destination_root, initializer_path) return if File.exist?(full_path) # Parser literal MUST stay byte-identical to app_template.rb # (drift test: locale_parser_drift_spec.rb). Can't share via require — # app_template.rb runs before Bundler loads the gem's lib/. # `.present?` (not `if options[:locale]`) — Thor passes empty --locale= # through as truthy "", which would otherwise yield broken locales: []. i18n_check_locales = if [:locale].present? [:locale].split(",").map(&:strip).reject(&:empty?).uniq.map(&:to_sym) else [ :en ] end create_file initializer_path, <<~RUBY # frozen_string_literal: true LcpRuby.configure do |config| # Authentication mode: :none, :built_in, or :external # :none — no auth required; ApplicationController exposes a # stub current_user with role "admin" (see the # ApplicationController file). Suitable for prototypes. # :built_in — set automatically by `rails generate lcp_ruby:install_auth` # (Devise-based; flips this line and removes the stub). # :external — host app provides current_user (Devise, Sorcery, custom). config.authentication = :none # User class name (default: "User") # config.user_class = "User" # Role method on user object (default: :lcp_role) # config.role_method = :lcp_role # Where users land after hitting "/" (defaults to first menu item alphabetically). # String form: config.landing_page = "dashboard" # Per-role: config.landing_page = { "admin" => "admin-home", "default" => "dashboard" } # See docs/reference/engine-configuration.md#landing_page # config.landing_page = nil # Type-driven renderer defaults: when true, presenter columns # whose `renderer:` is absent get the type-driven default at # runtime (e.g. :boolean → boolean_icon, :enum → badge, # :text → truncate). Decision 14 in # docs/design/type_system_defaults.md. New apps default this to # true; existing apps upgrading should flip after auditing # their hand-written index columns. config.runtime_type_renderers = true # i18n_check (rake lcp_ruby:i18n_check) — boot-time lint that # asserts every labeled metadata element has a translation # in every locale below. See docs/reference/i18n_check.md. # # Locales: list ONLY the locales this app actually ships. # Without this, the lint walks I18n.available_locales which on # rails-i18n hosts is ~30 entries and produces unreadable # `missing in: en,ar,de,...` reports. # # Three offense kinds, each with its own severity: # * literal_in_dsl — hardcoded label paired with an # i18n key that's missing in some # locale. Real broken-UX bug → :error. # * direct_render_label — hardcoded label on a surface the # runtime renders directly without # an i18n lookup (column / filter / # view-group view labels, …). No # quick fix today → :warning. # * missing_translation — humanize-default fallback would # render in some locale → :warning # so the initial cleanup ramp # doesn't freeze every PR. config.i18n_check = { locales: #{i18n_check_locales.inspect}, severity_per_kind: { literal_in_dsl: :error, direct_render_label: :warning, missing_translation: :warning } } end # Auto-discover custom actions, event handlers, and condition services # from app/actions/, app/event_handlers/, and app/condition_services/. # `on_models_loaded` re-fires on every `LcpRuby.reload!` (autoreload # in dev), so the registries stay populated after metadata reloads; # `Rails.application.config.after_initialize` would only run once # per process boot. LcpRuby.on_models_loaded do app_path = Rails.root.join("app") LcpRuby::Actions::ActionRegistry.discover!(app_path.to_s) LcpRuby::Events::HandlerRegistry.discover!(app_path.to_s) end RUBY end |
#create_sample_model ⇒ Object
124 125 126 127 128 129 130 131 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 124 def create_sample_model return if [:skip_sample] copy_dsl_or_yaml "model.rb", dsl_target: "config/lcp_ruby/models/item.rb", yaml_target: "config/lcp_ruby/models/item.yml" prepend_sample_header!(yaml_format? ? "config/lcp_ruby/models/item.yml" : "config/lcp_ruby/models/item.rb") end |
#create_sample_permissions ⇒ Object
142 143 144 145 146 147 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 142 def return if [:skip_sample] template "permissions.yml", "config/lcp_ruby/permissions/item.yml" prepend_sample_header!("config/lcp_ruby/permissions/item.yml") end |
#create_sample_presenter ⇒ Object
133 134 135 136 137 138 139 140 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 133 def create_sample_presenter return if [:skip_sample] copy_dsl_or_yaml "presenter.rb", dsl_target: "config/lcp_ruby/presenters/items.rb", yaml_target: "config/lcp_ruby/presenters/items.yml" prepend_sample_header!(yaml_format? ? "config/lcp_ruby/presenters/items.yml" : "config/lcp_ruby/presenters/items.rb") end |
#create_sample_view_group ⇒ Object
149 150 151 152 153 154 155 156 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 149 def create_sample_view_group return if [:skip_sample] copy_dsl_or_yaml "view_group.rb", dsl_target: "config/lcp_ruby/views/items.rb", yaml_target: "config/lcp_ruby/views/items.yml" prepend_sample_header!(yaml_format? ? "config/lcp_ruby/views/items.yml" : "config/lcp_ruby/views/items.rb") end |
#install_active_storage ⇒ Object
Active Storage backs the ‘attachment` field type and `rich_text` content, both of which are core to LCP. Auto-invoke is idempotent: if Active Storage migrations already exist, the call is skipped.
Opt out with ‘–no-active-storage` if the host application uses external storage (e.g. a custom gem) or doesn’t need attachments at all.
346 347 348 349 350 351 352 353 354 355 356 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 346 def install_active_storage return unless [:active_storage] if active_storage_already_installed? say_status :skip, "active_storage:install (migrations already present)" return end say_status :run, "active_storage:install" invoke_active_storage_install! end |
#install_asset_pipeline_compat ⇒ Object
Sprockets compatibility shim (audit #16). Rails 8 ships Propshaft by default, which doesn’t understand the ‘//= require` directives LCP’s JS bundle uses. Without this shim, the layout’s ‘<script>` tags either don’t render (the old ‘respond_to?(:assets)` gate) or render but 404 — both present as “top nav invisible, no JS errors, no console output.” The engine’s boot check (‘check_asset_pipeline_compat!`) raises if Propshaft is loaded without sprockets-rails; this method is the one-shot fix for that error.
Idempotent: skips Gemfile insertion if the gem line already exists; appends only missing link directives to an existing manifest.js.
111 112 113 114 115 116 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 111 def install_asset_pipeline_compat return if [:skip_asset_pipeline] add_sprockets_rails_to_gemfile! ensure_sprockets_manifest! end |
#mount_engine ⇒ Object
96 97 98 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 96 def mount_engine route 'mount LcpRuby::Engine => "/"' end |
#setup_agent_affordances ⇒ Object
Agent affordances: copies the lcp-* skills and writes CLAUDE.md/AGENTS.md so AI agents (Claude Code, Codex, …) have orientation + authoring skills. Passive — no runtime impact. Idempotent via the managed-block strategy. Invoked in-process (not a ‘generate` subprocess) so it shares this run’s destination_root and stays robust in tests/host apps alike. Explicit empty args avoid bleeding install’s options into the sibling generator.
435 436 437 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 435 def setup_agent_affordances invoke "lcp_ruby:agent_setup", [], {} end |
#show_post_install_message ⇒ Object
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 |
# File 'lib/generators/lcp_ruby/install_generator.rb', line 439 def say "" say "LCP Ruby installed!", :green say "" unless [:skip_sample] say "Sample files (each carries a 'DELETE ME' header — remove when you add your own entities):" say " config/lcp_ruby/models/item.#{format_extension}" say " config/lcp_ruby/presenters/items.#{format_extension}" say " config/lcp_ruby/permissions/item.yml" say " config/lcp_ruby/views/items.#{format_extension}" say "" end say "Tip: a commented user-menu block has been added to " \ "config/lcp_ruby/menu.yml. Run `rails generate lcp_ruby:install_auth` " \ "to auto-activate it (with Devise sign-out wired in), or " \ "uncomment manually after wiring up your auth routes." say "" say "Next steps:" say " 1. rails db:prepare" say " 2. rails s" if [:skip_sample] say " 3. Add your first entity: rails generate lcp_ruby:entity Foo name:string" else say " 3. Visit http://localhost:3000/items" end say "" say "Add features with generators:" say " rails generate lcp_ruby:install_auth # Devise authentication" say " rails generate lcp_ruby:auditing # Audit trail" say " rails generate lcp_ruby:export # Data export" say " rails generate lcp_ruby:import # Data import" say " rails generate lcp_ruby:custom_fields # Runtime field definitions" say " rails generate lcp_ruby:saved_filters # Saved search filters" say " rails generate lcp_ruby:monitoring # Error dashboard" say " rails generate lcp_ruby:batch_operations # Bulk operations" say " rails generate lcp_ruby:background_jobs # Job tracking" say " rails generate lcp_ruby:pages # DB-backed pages" say " rails generate lcp_ruby:groups # User groups" say " rails generate lcp_ruby:role_model # DB-backed roles" say " rails generate lcp_ruby:permission_source # Dynamic permissions" say " rails generate lcp_ruby:workflow_definition # Workflows" say " rails generate lcp_ruby:workflow_approvals # Approval processes" say " rails generate lcp_ruby:workflow_audit_log # Workflow audit" say " rails generate lcp_ruby:gapfree_sequences # Sequential numbering" say "" end |