Class: LcpRuby::Generators::InstallGenerator

Inherits:
Rails::Generators::Base
  • Object
show all
Includes:
FormatSupport
Defined in:
lib/generators/lcp_ruby/install_generator.rb

Constant Summary

Constants included from FormatSupport

FormatSupport::VALID_FORMATS

Instance Method Summary collapse

Methods included from FormatSupport

included, #validate_format

Instance Method Details

#add_autoloader_ignoreObject



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/generators/lcp_ruby/install_generator.rb', line 41

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_helperObject



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/generators/lcp_ruby/install_generator.rb', line 213

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_requireObject



30
31
32
33
34
35
36
37
38
39
# File 'lib/generators/lcp_ruby/install_generator.rb', line 30

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

#create_default_menuObject

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.



113
114
115
# File 'lib/generators/lcp_ruby/install_generator.rb', line 113

def create_default_menu
  template "menu.yml.tt", "config/lcp_ruby/menu.yml"
end

#create_default_permissionsObject



102
103
104
# File 'lib/generators/lcp_ruby/install_generator.rb', line 102

def create_default_permissions
  template "default_permissions.yml", "config/lcp_ruby/permissions/_default.yml"
end

#create_directory_structureObject



66
67
68
69
70
# File 'lib/generators/lcp_ruby/install_generator.rb', line 66

def create_directory_structure
  %w[models presenters permissions views].each do |dir|
    empty_directory "config/lcp_ruby/#{dir}"
  end
end

#create_initializerObject



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
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
# File 'lib/generators/lcp_ruby/install_generator.rb', line 117

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 options[:locale].present?
    options[: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_modelObject



72
73
74
75
76
77
78
# File 'lib/generators/lcp_ruby/install_generator.rb', line 72

def create_sample_model
  return if options[:skip_sample]

  copy_dsl_or_yaml "model.rb",
    dsl_target: "config/lcp_ruby/models/item.rb",
    yaml_target: "config/lcp_ruby/models/item.yml"
end

#create_sample_permissionsObject



88
89
90
91
92
# File 'lib/generators/lcp_ruby/install_generator.rb', line 88

def create_sample_permissions
  return if options[:skip_sample]

  template "permissions.yml", "config/lcp_ruby/permissions/item.yml"
end

#create_sample_presenterObject



80
81
82
83
84
85
86
# File 'lib/generators/lcp_ruby/install_generator.rb', line 80

def create_sample_presenter
  return if options[:skip_sample]

  copy_dsl_or_yaml "presenter.rb",
    dsl_target: "config/lcp_ruby/presenters/items.rb",
    yaml_target: "config/lcp_ruby/presenters/items.yml"
end

#create_sample_view_groupObject



94
95
96
97
98
99
100
# File 'lib/generators/lcp_ruby/install_generator.rb', line 94

def create_sample_view_group
  return if options[: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"
end

#install_active_storageObject

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.



238
239
240
241
242
243
244
245
246
247
248
# File 'lib/generators/lcp_ruby/install_generator.rb', line 238

def install_active_storage
  return unless options[: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

#mount_engineObject



62
63
64
# File 'lib/generators/lcp_ruby/install_generator.rb', line 62

def mount_engine
  route 'mount LcpRuby::Engine => "/"'
end

#setup_agent_affordancesObject

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.



271
272
273
# File 'lib/generators/lcp_ruby/install_generator.rb', line 271

def setup_agent_affordances
  invoke "lcp_ruby:agent_setup", [], {}
end

#show_post_install_messageObject



275
276
277
278
279
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
# File 'lib/generators/lcp_ruby/install_generator.rb', line 275

def show_post_install_message
  say ""
  say "LCP Ruby installed!", :green
  say ""
  unless options[:skip_sample]
    say "Sample files:"
    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"
  say "  3. Visit http://localhost:3000/items"
  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