lcp 0.1.0 → 0.2.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/lcp-custom-field/SKILL.md +10 -1
- data/.claude/skills/lcp-custom-field/agents/openai.yaml +4 -0
- data/.claude/skills/lcp-getting-started/SKILL.md +1 -1
- data/.claude/skills/lcp-getting-started/agents/openai.yaml +4 -0
- data/.claude/skills/lcp-host-binding/agents/openai.yaml +4 -0
- data/.claude/skills/lcp-model/agents/openai.yaml +4 -0
- data/.claude/skills/lcp-permissions/agents/openai.yaml +4 -0
- data/.claude/skills/lcp-presenter/agents/openai.yaml +4 -0
- data/.claude/skills/lcp-workflow/SKILL.md +10 -2
- data/.claude/skills/lcp-workflow/agents/openai.yaml +4 -0
- data/CHANGELOG.md +59 -1
- data/app/assets/javascripts/lcp_ruby/dev_toolbar.js +5 -6
- data/app/controllers/concerns/lcp_ruby/zone_resolution.rb +14 -1
- data/app/helpers/lcp_ruby/display/card_helper.rb +71 -16
- data/app/helpers/lcp_ruby/display_helper.rb +30 -3
- data/app/helpers/lcp_ruby/display_template_helper.rb +3 -3
- data/app/helpers/lcp_ruby/layout_helper.rb +49 -8
- data/app/helpers/lcp_ruby/tree_helper.rb +24 -3
- data/app/models/lcp_ruby/user.rb +10 -0
- data/app/views/layouts/lcp_ruby/application.html.erb +42 -20
- data/app/views/layouts/lcp_ruby/auth.html.erb +6 -1
- data/app/views/lcp_ruby/resources/_show_sections.html.erb +24 -17
- data/app/views/lcp_ruby/resources/_table_index.html.erb +12 -17
- data/app/views/lcp_ruby/widgets/_markdown.html.erb +13 -0
- data/app/views/lcp_ruby/widgets/_presenter_zone.html.erb +23 -7
- data/app/views/lcp_ruby/widgets/_rich_text.html.erb +25 -0
- data/docs/README.md +3 -0
- data/docs/feature-catalog.md +5 -2
- data/docs/feature-catalog.yml +89 -9
- data/docs/getting-started.md +38 -22
- data/docs/guides/dashboards.md +44 -1
- data/docs/guides/display-types.md +6 -0
- data/docs/guides/host-application.md +1 -1
- data/docs/guides/windows-setup.md +211 -0
- data/docs/guides/workflow.md +1 -1
- data/docs/reference/asset-pipeline.md +79 -0
- data/docs/reference/pages.md +50 -1
- data/docs/reference/presenters.md +6 -0
- data/docs/reference/workflow.md +2 -2
- data/examples/crm/Gemfile +3 -0
- data/examples/crm/Gemfile.lock +14 -9
- data/examples/crm/app/assets/config/manifest.js +4 -0
- data/examples/hr/Gemfile +3 -0
- data/examples/hr/Gemfile.lock +14 -9
- data/examples/hr/app/assets/config/manifest.js +4 -0
- data/examples/showcase/Gemfile +3 -0
- data/examples/showcase/Gemfile.lock +12 -9
- data/examples/showcase/app/assets/config/manifest.js +4 -0
- data/examples/showcase/config/lcp_ruby/pages/main_dashboard.yml +17 -2
- data/examples/showcase/config/locales/cs.yml +8 -0
- data/examples/showcase/config/locales/en.yml +8 -0
- data/examples/todo/Gemfile +3 -0
- data/examples/todo/Gemfile.lock +14 -9
- data/examples/todo/app/assets/config/manifest.js +4 -0
- data/exe/lcp +1 -1
- data/lib/generators/lcp_ruby/agent_setup_generator.rb +12 -9
- data/lib/generators/lcp_ruby/claude_skills_generator.rb +23 -19
- data/lib/generators/lcp_ruby/install_generator.rb +171 -3
- data/lib/generators/lcp_ruby/templates/agent_setup/agents_md.md +2 -1
- data/lib/generators/lcp_ruby/templates/agent_setup/claude_md.md +3 -2
- data/lib/generators/lcp_ruby/templates/install/menu.yml.tt +4 -0
- data/lib/generators/lcp_ruby/templates/monitoring/page.yml +5 -0
- data/lib/lcp_ruby/app_template.rb +37 -1
- data/lib/lcp_ruby/array_query.rb +33 -10
- data/lib/lcp_ruby/cli/new_command.rb +9 -4
- data/lib/lcp_ruby/cli/run_command.rb +98 -11
- data/lib/lcp_ruby/cli/skills_command.rb +27 -15
- data/lib/lcp_ruby/cli.rb +5 -1
- data/lib/lcp_ruby/configuration.rb +13 -2
- data/lib/lcp_ruby/custom_fields/applicator.rb +8 -0
- data/lib/lcp_ruby/custom_fields/query.rb +28 -20
- data/lib/lcp_ruby/display/markdown_sanitize.rb +36 -0
- data/lib/lcp_ruby/display/renderers/markdown.rb +6 -13
- data/lib/lcp_ruby/engine.rb +185 -1
- data/lib/lcp_ruby/export/data_generator.rb +5 -1
- data/lib/lcp_ruby/export/value_formatter.rb +34 -0
- data/lib/lcp_ruby/grouped_query/builder.rb +21 -0
- data/lib/lcp_ruby/metadata/configuration_validator.rb +5 -5
- data/lib/lcp_ruby/metadata/field_path.rb +57 -0
- data/lib/lcp_ruby/metadata/loader.rb +33 -1
- data/lib/lcp_ruby/metadata/menu_definition.rb +32 -2
- data/lib/lcp_ruby/metadata/menu_item.rb +53 -4
- data/lib/lcp_ruby/metadata/model_definition.rb +9 -0
- data/lib/lcp_ruby/metadata/zone_definition.rb +12 -5
- data/lib/lcp_ruby/metrics/json_query.rb +5 -0
- data/lib/lcp_ruby/model_factory/builder.rb +5 -0
- data/lib/lcp_ruby/model_factory/json_type_applicator.rb +35 -0
- data/lib/lcp_ruby/model_factory/label_method_builder.rb +3 -19
- data/lib/lcp_ruby/model_factory/schema_manager.rb +19 -0
- data/lib/lcp_ruby/pages/definition_validator.rb +13 -0
- data/lib/lcp_ruby/presenter/field_value_resolver.rb +36 -19
- data/lib/lcp_ruby/schemas/page.json +33 -1
- data/lib/lcp_ruby/search/custom_field_filter.rb +5 -0
- data/lib/lcp_ruby/skills_installer.rb +15 -2
- data/lib/lcp_ruby/tasks/doctor.rb +13 -8
- data/lib/lcp_ruby/version.rb +1 -1
- data/lib/lcp_ruby/widgets/data_resolver.rb +42 -1
- data/lib/lcp_ruby/widgets/date_grouper.rb +19 -0
- data/lib/lcp_ruby/workflow/contract_validator.rb +1 -1
- data/lib/lcp_ruby/workflow/model_source.rb +2 -2
- data/lib/lcp_ruby.rb +18 -0
- data/lib/tasks/lcp_ruby_db.rake +8 -1
- data/lib/tasks/lcp_ruby_i18n_check.rake +8 -0
- metadata +34 -24
- data/examples/crm/erd.md +0 -163
- data/examples/hr/erd.md +0 -396
- data/examples/showcase/erd.md +0 -567
- data/examples/todo/erd.md +0 -21
|
@@ -5,41 +5,45 @@ require "lcp_ruby/skills_installer"
|
|
|
5
5
|
|
|
6
6
|
module LcpRuby
|
|
7
7
|
module Generators
|
|
8
|
-
# Installs LCP-specific
|
|
9
|
-
#
|
|
8
|
+
# Legacy skills-only in-app entry. Installs LCP-specific AI authoring skills
|
|
9
|
+
# into all supported agent skill directories by copying them from the gem's
|
|
10
|
+
# bundled `.claude/skills/` source.
|
|
10
11
|
# Re-running refreshes the skills to the version shipped with the currently
|
|
11
12
|
# installed gem (useful after `bundle update lcp`) and prunes any `lcp-*`
|
|
12
13
|
# skills the gem no longer ships. Non-`lcp-*` skills are never touched.
|
|
13
14
|
#
|
|
14
|
-
# This is the skills-only in-app entry; `lcp_ruby:agent_setup`
|
|
15
|
-
#
|
|
16
|
-
#
|
|
15
|
+
# This is the skills-only in-app entry; `lcp_ruby:agent_setup` uses the same
|
|
16
|
+
# targets and additionally writes the CLAUDE.md/AGENTS.md entrypoint. Both
|
|
17
|
+
# call the installer directly (no generator-calls-generator).
|
|
17
18
|
class ClaudeSkillsGenerator < Rails::Generators::Base
|
|
18
|
-
desc "Installs LCP Ruby
|
|
19
|
+
desc "Installs LCP Ruby AI agent skills into .claude/skills/ and .codex/skills/"
|
|
19
20
|
|
|
20
21
|
def install_skills
|
|
21
|
-
@
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
@results = {}
|
|
23
|
+
LcpRuby::SkillsInstaller.agent_skill_targets(root: destination_root).each do |agent, target|
|
|
24
|
+
result = LcpRuby::SkillsInstaller.new(target: target).call
|
|
25
|
+
@results[agent] = result
|
|
24
26
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
relative_target = File.join(LcpRuby::SkillsInstaller::AGENT_DIRECTORIES.fetch(agent), "skills")
|
|
28
|
+
result.created.each { |n| say_status :create, "#{relative_target}/#{n}", :green }
|
|
29
|
+
result.updated.each { |n| say_status :update, "#{relative_target}/#{n}", :yellow }
|
|
30
|
+
result.removed.each { |n| say_status :remove, "#{relative_target}/#{n}", :red }
|
|
31
|
+
end
|
|
28
32
|
end
|
|
29
33
|
|
|
30
34
|
def show_post_install_message
|
|
31
|
-
return if @
|
|
35
|
+
return if @results.nil? || @results.values.none?(&:any_changes?)
|
|
32
36
|
|
|
33
|
-
installed =
|
|
37
|
+
installed = @results.values.flat_map { |result| result.created + result.updated }.uniq.sort
|
|
34
38
|
say ""
|
|
35
|
-
say "Installed #{installed.size} LCP Ruby
|
|
39
|
+
say "Installed #{installed.size} LCP Ruby AI agent skill(s):", :green
|
|
36
40
|
installed.each { |n| say " - #{n}" }
|
|
37
41
|
say ""
|
|
38
|
-
say "Skills auto-trigger in Claude Code based on context (description keywords)."
|
|
39
|
-
say "List available skills via /skills inside Claude Code."
|
|
42
|
+
say "Skills auto-trigger in Claude Code and Codex based on context (description keywords)."
|
|
43
|
+
say "List available skills via /skills inside Claude Code or the Codex skills list."
|
|
40
44
|
say ""
|
|
41
|
-
say "
|
|
42
|
-
say " bin/rails generate lcp_ruby:
|
|
45
|
+
say "Preferred refresh after `bundle update lcp`:"
|
|
46
|
+
say " bin/rails generate lcp_ruby:agent_setup"
|
|
43
47
|
say ""
|
|
44
48
|
end
|
|
45
49
|
end
|
|
@@ -13,11 +13,45 @@ module LcpRuby
|
|
|
13
13
|
desc "Sets up LCP Ruby in an existing Rails application"
|
|
14
14
|
|
|
15
15
|
class_option :skip_sample, type: :boolean, default: false,
|
|
16
|
-
desc: "Skip
|
|
16
|
+
desc: "Skip the sample `item` model + presenter + permissions + view group. Default: create them — a Hello-World scaffold so a fresh install has something to click at /items. Each generated file carries a DELETE ME header pointing at all four siblings."
|
|
17
|
+
|
|
18
|
+
# Header injected into each sample file (DSL + YAML both use `#` for
|
|
19
|
+
# comments, so one literal works for both formats). DslToYaml strips
|
|
20
|
+
# in-source comments during conversion, so we prepend after copy via
|
|
21
|
+
# `prepend_sample_header!` rather than embedding in the template.
|
|
22
|
+
SAMPLE_HEADER = <<~HEADER
|
|
23
|
+
# DELETE ME — sample scaffold for first-day discovery.
|
|
24
|
+
# `lcp_ruby:install` (and `lcp new` without `--skip-sample`) seeds
|
|
25
|
+
# this `item` entity so a fresh install has something to click at
|
|
26
|
+
# /items. Once you've added your own entities via
|
|
27
|
+
# `rails g lcp_ruby:entity`, delete all four sample files:
|
|
28
|
+
# config/lcp_ruby/models/item.{rb,yml}
|
|
29
|
+
# config/lcp_ruby/presenters/items.{rb,yml}
|
|
30
|
+
# config/lcp_ruby/permissions/item.yml
|
|
31
|
+
# config/lcp_ruby/views/items.{rb,yml}
|
|
32
|
+
# ...and remove `- view_group: items` from config/lcp_ruby/menu.yml.
|
|
33
|
+
|
|
34
|
+
HEADER
|
|
17
35
|
|
|
18
36
|
class_option :active_storage, type: :boolean, default: true,
|
|
19
37
|
desc: "Install Active Storage migrations (idempotent — skipped if already present)"
|
|
20
38
|
|
|
39
|
+
class_option :skip_asset_pipeline, type: :boolean, default: false,
|
|
40
|
+
desc: "Skip the Sprockets-compatibility shim (don't add `sprockets-rails` or create app/assets/config/manifest.js). " \
|
|
41
|
+
"Default: install. Only opt out if you've wired your own ESM bundler — LCP's JS won't load without one or the other."
|
|
42
|
+
|
|
43
|
+
# Sprockets manifest entries required for LCP's JS + CSS to be
|
|
44
|
+
# served by `javascript_include_tag` / `stylesheet_link_tag`. Used by
|
|
45
|
+
# `install_asset_pipeline_compat` (this generator) and mirrored in
|
|
46
|
+
# the engine's `check_asset_pipeline_compat!` error message — keep
|
|
47
|
+
# the two in sync.
|
|
48
|
+
SPROCKETS_MANIFEST_LINKS = [
|
|
49
|
+
"//= link lcp_ruby/application.js",
|
|
50
|
+
"//= link lcp_ruby/application.css",
|
|
51
|
+
"//= link lcp_ruby/tom-select.css",
|
|
52
|
+
"//= link lcp_ruby/tom-select.complete.min.js"
|
|
53
|
+
].freeze
|
|
54
|
+
|
|
21
55
|
class_option :menu_layout, type: :string, default: "both", enum: %w[top sidebar both],
|
|
22
56
|
desc: "Menu layout sections to seed (top, sidebar, or both — default: both). " \
|
|
23
57
|
"Seeds `# lcp:menu <section>` insertion markers for `rails g lcp_ruby:entity --menu=...`."
|
|
@@ -63,6 +97,24 @@ module LcpRuby
|
|
|
63
97
|
route 'mount LcpRuby::Engine => "/"'
|
|
64
98
|
end
|
|
65
99
|
|
|
100
|
+
# Sprockets compatibility shim (audit #16). Rails 8 ships Propshaft
|
|
101
|
+
# by default, which doesn't understand the `//= require` directives
|
|
102
|
+
# LCP's JS bundle uses. Without this shim, the layout's `<script>`
|
|
103
|
+
# tags either don't render (the old `respond_to?(:assets)` gate) or
|
|
104
|
+
# render but 404 — both present as "top nav invisible, no JS errors,
|
|
105
|
+
# no console output." The engine's boot check (`check_asset_pipeline_compat!`)
|
|
106
|
+
# raises if Propshaft is loaded without sprockets-rails; this method
|
|
107
|
+
# is the one-shot fix for that error.
|
|
108
|
+
#
|
|
109
|
+
# Idempotent: skips Gemfile insertion if the gem line already exists;
|
|
110
|
+
# appends only missing link directives to an existing manifest.js.
|
|
111
|
+
def install_asset_pipeline_compat
|
|
112
|
+
return if options[:skip_asset_pipeline]
|
|
113
|
+
|
|
114
|
+
add_sprockets_rails_to_gemfile!
|
|
115
|
+
ensure_sprockets_manifest!
|
|
116
|
+
end
|
|
117
|
+
|
|
66
118
|
def create_directory_structure
|
|
67
119
|
%w[models presenters permissions views].each do |dir|
|
|
68
120
|
empty_directory "config/lcp_ruby/#{dir}"
|
|
@@ -75,6 +127,7 @@ module LcpRuby
|
|
|
75
127
|
copy_dsl_or_yaml "model.rb",
|
|
76
128
|
dsl_target: "config/lcp_ruby/models/item.rb",
|
|
77
129
|
yaml_target: "config/lcp_ruby/models/item.yml"
|
|
130
|
+
prepend_sample_header!(yaml_format? ? "config/lcp_ruby/models/item.yml" : "config/lcp_ruby/models/item.rb")
|
|
78
131
|
end
|
|
79
132
|
|
|
80
133
|
def create_sample_presenter
|
|
@@ -83,12 +136,14 @@ module LcpRuby
|
|
|
83
136
|
copy_dsl_or_yaml "presenter.rb",
|
|
84
137
|
dsl_target: "config/lcp_ruby/presenters/items.rb",
|
|
85
138
|
yaml_target: "config/lcp_ruby/presenters/items.yml"
|
|
139
|
+
prepend_sample_header!(yaml_format? ? "config/lcp_ruby/presenters/items.yml" : "config/lcp_ruby/presenters/items.rb")
|
|
86
140
|
end
|
|
87
141
|
|
|
88
142
|
def create_sample_permissions
|
|
89
143
|
return if options[:skip_sample]
|
|
90
144
|
|
|
91
145
|
template "permissions.yml", "config/lcp_ruby/permissions/item.yml"
|
|
146
|
+
prepend_sample_header!("config/lcp_ruby/permissions/item.yml")
|
|
92
147
|
end
|
|
93
148
|
|
|
94
149
|
def create_sample_view_group
|
|
@@ -97,6 +152,7 @@ module LcpRuby
|
|
|
97
152
|
copy_dsl_or_yaml "view_group.rb",
|
|
98
153
|
dsl_target: "config/lcp_ruby/views/items.rb",
|
|
99
154
|
yaml_target: "config/lcp_ruby/views/items.yml"
|
|
155
|
+
prepend_sample_header!(yaml_format? ? "config/lcp_ruby/views/items.yml" : "config/lcp_ruby/views/items.rb")
|
|
100
156
|
end
|
|
101
157
|
|
|
102
158
|
def create_default_permissions
|
|
@@ -210,6 +266,58 @@ module LcpRuby
|
|
|
210
266
|
RUBY
|
|
211
267
|
end
|
|
212
268
|
|
|
269
|
+
# When the Sprockets compatibility shim is skipped, the engine's boot
|
|
270
|
+
# check would otherwise raise LcpRuby::AssetPipelineError on the next
|
|
271
|
+
# non-generator boot. Write the matching opt-out so the documented
|
|
272
|
+
# `--skip-asset-pipeline` flag yields a bootable app.
|
|
273
|
+
#
|
|
274
|
+
# MUST stay a separate step from `create_initializer` (which early-returns
|
|
275
|
+
# when the initializer already exists): on a re-run or install over a
|
|
276
|
+
# pre-existing initializer, the opt-out has to be applied regardless of
|
|
277
|
+
# whether the file was freshly created — otherwise the flag silently
|
|
278
|
+
# no-ops and the app won't boot. Idempotent: skips when the line is
|
|
279
|
+
# already present.
|
|
280
|
+
def configure_skip_asset_pipeline
|
|
281
|
+
return unless options[:skip_asset_pipeline]
|
|
282
|
+
|
|
283
|
+
initializer_path = "config/initializers/lcp_ruby.rb"
|
|
284
|
+
full_path = File.join(destination_root, initializer_path)
|
|
285
|
+
unless File.exist?(full_path)
|
|
286
|
+
say_status :skip, "skip_asset_pipeline_check (no #{initializer_path})", :yellow
|
|
287
|
+
return
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
content = File.read(full_path)
|
|
291
|
+
return if content.include?("skip_asset_pipeline_check")
|
|
292
|
+
|
|
293
|
+
# Match the configure block (both `do |x|` and brace `{ |x|` forms, the
|
|
294
|
+
# latter is what the engine's AssetPipelineError message suggests) and
|
|
295
|
+
# CAPTURE its block-variable name so the injected line targets the host's
|
|
296
|
+
# actual var (`config`, `c`, …) — a hardcoded `config.` would NameError
|
|
297
|
+
# at boot on a `|c|` initializer. Not anchored to a trailing newline, so
|
|
298
|
+
# a `do |config| # comment` line still matches.
|
|
299
|
+
match = content.match(/LcpRuby\.configure\s*(?:do|\{)\s*\|\s*(\w+)\s*\|/)
|
|
300
|
+
unless match
|
|
301
|
+
say_status :warn,
|
|
302
|
+
"could not anchor skip_asset_pipeline_check in #{initializer_path}; " \
|
|
303
|
+
"add `config.skip_asset_pipeline_check = true` inside the LcpRuby.configure block manually",
|
|
304
|
+
:yellow
|
|
305
|
+
return
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
var = match[1]
|
|
309
|
+
optout = <<~RUBY.indent(2)
|
|
310
|
+
# --skip-asset-pipeline was passed: the Sprockets compatibility shim
|
|
311
|
+
# is NOT installed, so the boot-time asset-pipeline check is opted
|
|
312
|
+
# out too (otherwise the engine raises LcpRuby::AssetPipelineError).
|
|
313
|
+
# Only correct if you've wired your own JS bundler (esbuild,
|
|
314
|
+
# importmap with a custom pin map, jsbundling, etc.).
|
|
315
|
+
#{var}.skip_asset_pipeline_check = true
|
|
316
|
+
RUBY
|
|
317
|
+
|
|
318
|
+
inject_into_file initializer_path, "\n#{optout}", after: match[0]
|
|
319
|
+
end
|
|
320
|
+
|
|
213
321
|
def add_current_user_helper
|
|
214
322
|
controller_path = "app/controllers/application_controller.rb"
|
|
215
323
|
full_path = File.join(destination_root, controller_path)
|
|
@@ -260,6 +368,62 @@ module LcpRuby
|
|
|
260
368
|
def invoke_active_storage_install!
|
|
261
369
|
rails_command "active_storage:install", abort_on_failure: false
|
|
262
370
|
end
|
|
371
|
+
|
|
372
|
+
# Thor's `prepend_to_file` is idempotent on string match but we
|
|
373
|
+
# also want to be no-op on rerun where the file was overwritten
|
|
374
|
+
# by `template`. Cheapest guard: skip when the header is already
|
|
375
|
+
# present at the top of the file.
|
|
376
|
+
def prepend_sample_header!(relative_path)
|
|
377
|
+
full_path = File.join(destination_root, relative_path)
|
|
378
|
+
return unless File.exist?(full_path)
|
|
379
|
+
return if File.read(full_path).start_with?("# DELETE ME")
|
|
380
|
+
prepend_to_file relative_path, SAMPLE_HEADER
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
# Idempotent Gemfile insertion. `gem "sprockets-rails"` matches
|
|
384
|
+
# any quote style or version constraint so re-runs don't duplicate.
|
|
385
|
+
def add_sprockets_rails_to_gemfile!
|
|
386
|
+
gemfile_path = File.join(destination_root, "Gemfile")
|
|
387
|
+
unless File.exist?(gemfile_path)
|
|
388
|
+
say_status :skip, "sprockets-rails (no Gemfile — manual install required)", :yellow
|
|
389
|
+
return
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
if File.read(gemfile_path).match?(/^\s*gem\s+["']sprockets-rails["']/)
|
|
393
|
+
say_status :identical, "Gemfile already has sprockets-rails"
|
|
394
|
+
return
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
gem "sprockets-rails"
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Creates or updates `app/assets/config/manifest.js`. If the file
|
|
401
|
+
# exists, only the missing `//= link` directives are appended —
|
|
402
|
+
# idempotent across re-runs and friendly to hosts that already
|
|
403
|
+
# have host-specific entries (e.g. `Chart.bundle.js` for charts).
|
|
404
|
+
def ensure_sprockets_manifest!
|
|
405
|
+
manifest_path = "app/assets/config/manifest.js"
|
|
406
|
+
full_path = File.join(destination_root, manifest_path)
|
|
407
|
+
|
|
408
|
+
if File.exist?(full_path)
|
|
409
|
+
existing = File.read(full_path)
|
|
410
|
+
missing = SPROCKETS_MANIFEST_LINKS.reject { |line| existing.include?(line) }
|
|
411
|
+
if missing.empty?
|
|
412
|
+
say_status :identical, manifest_path
|
|
413
|
+
else
|
|
414
|
+
append_to_file manifest_path, "\n# Added by lcp_ruby:install\n#{missing.join("\n")}\n"
|
|
415
|
+
end
|
|
416
|
+
else
|
|
417
|
+
create_file manifest_path, <<~MANIFEST
|
|
418
|
+
# LCP Ruby asset manifest (audit #16).
|
|
419
|
+
# `//= link` directives tell Sprockets to precompile + serve
|
|
420
|
+
# the listed files. The engine's JS bundle (~50 files) and
|
|
421
|
+
# CSS bundle are the load-bearing entries — without them
|
|
422
|
+
# the top nav, row-click, and form widgets are inert.
|
|
423
|
+
#{SPROCKETS_MANIFEST_LINKS.join("\n")}
|
|
424
|
+
MANIFEST
|
|
425
|
+
end
|
|
426
|
+
end
|
|
263
427
|
end
|
|
264
428
|
|
|
265
429
|
# Agent affordances: copies the lcp-* skills and writes CLAUDE.md/AGENTS.md
|
|
@@ -277,7 +441,7 @@ module LcpRuby
|
|
|
277
441
|
say "LCP Ruby installed!", :green
|
|
278
442
|
say ""
|
|
279
443
|
unless options[:skip_sample]
|
|
280
|
-
say "Sample files:"
|
|
444
|
+
say "Sample files (each carries a 'DELETE ME' header — remove when you add your own entities):"
|
|
281
445
|
say " config/lcp_ruby/models/item.#{format_extension}"
|
|
282
446
|
say " config/lcp_ruby/presenters/items.#{format_extension}"
|
|
283
447
|
say " config/lcp_ruby/permissions/item.yml"
|
|
@@ -292,7 +456,11 @@ module LcpRuby
|
|
|
292
456
|
say "Next steps:"
|
|
293
457
|
say " 1. rails db:prepare"
|
|
294
458
|
say " 2. rails s"
|
|
295
|
-
|
|
459
|
+
if options[:skip_sample]
|
|
460
|
+
say " 3. Add your first entity: rails generate lcp_ruby:entity Foo name:string"
|
|
461
|
+
else
|
|
462
|
+
say " 3. Visit http://localhost:3000/items"
|
|
463
|
+
end
|
|
296
464
|
say ""
|
|
297
465
|
say "Add features with generators:"
|
|
298
466
|
say " rails generate lcp_ruby:install_auth # Devise authentication"
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
This project uses **LCP Ruby**. See `CLAUDE.md` for orientation (project
|
|
2
2
|
layout, skills, docs, validation) — the same guidance applies to all agents.
|
|
3
|
-
For authoring patterns,
|
|
3
|
+
For authoring patterns, use the installed skills in `.codex/skills/lcp-*` or
|
|
4
|
+
`.claude/skills/lcp-*`.
|
|
@@ -2,8 +2,9 @@ This is an **LCP Ruby** project (low-code platform). App behavior is metadata
|
|
|
2
2
|
under `config/lcp_ruby/`: `models/` (data), `presenters/` (UI),
|
|
3
3
|
`permissions/` (roles).
|
|
4
4
|
|
|
5
|
-
- **Authoring guidance:** skills in `.claude/skills/lcp-*`
|
|
6
|
-
|
|
5
|
+
- **Authoring guidance:** skills in `.claude/skills/lcp-*` for Claude Code and
|
|
6
|
+
`.codex/skills/lcp-*` for Codex. Both directories contain the same `SKILL.md`
|
|
7
|
+
guidance and auto-trigger in their respective agent.
|
|
7
8
|
- **Reference docs:** in-app `bundle exec rake lcp_ruby:docs` (matches the
|
|
8
9
|
bundled gem version), or `lcp docs path` globally — start at `docs/README.md`.
|
|
9
10
|
When a skill or doc says `docs/X`, resolve it under that path.
|
|
@@ -6,7 +6,9 @@ menu:
|
|
|
6
6
|
<% if %w[top both].include?(options[:menu_layout]) -%>
|
|
7
7
|
# >>> LCP_USER_MENU_BLOCK (rewritten by lcp_ruby:install_auth) <<<
|
|
8
8
|
top_menu:
|
|
9
|
+
<% unless options[:skip_sample] -%>
|
|
9
10
|
- view_group: items
|
|
11
|
+
<% end -%>
|
|
10
12
|
# lcp:menu top # entity generator marker - do not edit/delete
|
|
11
13
|
|
|
12
14
|
# Uncomment when adding authentication, or run
|
|
@@ -62,7 +64,9 @@ menu:
|
|
|
62
64
|
<% end -%>
|
|
63
65
|
<% if options[:menu_layout] == "sidebar" -%>
|
|
64
66
|
sidebar_menu:
|
|
67
|
+
<% unless options[:skip_sample] -%>
|
|
65
68
|
- view_group: items
|
|
69
|
+
<% end -%>
|
|
66
70
|
# lcp:menu sidebar # entity generator marker - do not edit/delete
|
|
67
71
|
<% elsif options[:menu_layout] == "both" -%>
|
|
68
72
|
|
|
@@ -2,6 +2,11 @@ page:
|
|
|
2
2
|
name: monitoring_dashboard
|
|
3
3
|
slug: monitoring
|
|
4
4
|
layout: grid
|
|
5
|
+
# Operator-facing dashboard (error counts, metrics, recent errors).
|
|
6
|
+
# Restricted to admin by default; relax this when other roles need read access.
|
|
7
|
+
visible_when:
|
|
8
|
+
service: current_user_role
|
|
9
|
+
params: { in: [admin] }
|
|
5
10
|
|
|
6
11
|
zones:
|
|
7
12
|
- name: models_loaded
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
# LCP_TIMEZONE - default timezone (default: "UTC")
|
|
12
12
|
# LCP_LOCALE - default locale (default: "en")
|
|
13
13
|
# LCP_MENU_LAYOUT - "top", "sidebar", or "both" (default: "both")
|
|
14
|
+
# LCP_SKIP_SAMPLE - "1" to skip the Hello-World item model, "0" (default) to scaffold it
|
|
15
|
+
# LCP_SKIP_ASSET_PIPELINE - "1" to skip Sprockets compat shim (only for ESM-bundler users), "0" (default) to install it
|
|
14
16
|
|
|
15
17
|
require "json"
|
|
16
18
|
|
|
@@ -22,6 +24,8 @@ gem_source = ENV.fetch("LCP_GEM_SOURCE", "rubygems")
|
|
|
22
24
|
timezone = ENV.fetch("LCP_TIMEZONE", "UTC")
|
|
23
25
|
locale = ENV.fetch("LCP_LOCALE", "en")
|
|
24
26
|
menu_layout = ENV.fetch("LCP_MENU_LAYOUT", "both")
|
|
27
|
+
skip_sample = ENV.fetch("LCP_SKIP_SAMPLE", "0") == "1"
|
|
28
|
+
skip_asset_pipeline = ENV.fetch("LCP_SKIP_ASSET_PIPELINE", "0") == "1"
|
|
25
29
|
|
|
26
30
|
# Add lcp gem
|
|
27
31
|
case gem_source
|
|
@@ -35,6 +39,34 @@ else
|
|
|
35
39
|
gem "lcp"
|
|
36
40
|
end
|
|
37
41
|
|
|
42
|
+
# Sprockets compatibility shim (audit #16). Rails 8 ships Propshaft by
|
|
43
|
+
# default; LCP's JS uses Sprockets `//= require` directives that Propshaft
|
|
44
|
+
# can't parse, so without sprockets-rails the engine's nav + Stimulus
|
|
45
|
+
# controllers are silently inert. Add the gem at template-level so it's
|
|
46
|
+
# bundled before `after_bundle`'s `rails generate lcp_ruby:install` boots
|
|
47
|
+
# Rails (the install generator's `install_asset_pipeline_compat` step
|
|
48
|
+
# would otherwise hit the engine's loud boot check before getting to add
|
|
49
|
+
# the gem). The install generator's logic is still the source of truth —
|
|
50
|
+
# this hoist just sidesteps the chicken-and-egg.
|
|
51
|
+
gem "sprockets-rails" unless skip_asset_pipeline
|
|
52
|
+
|
|
53
|
+
# Create app/assets/config/manifest.js at template level too. sprockets-rails
|
|
54
|
+
# 3.5+ RAISES (ManifestNeededError) the moment its railtie loads without a
|
|
55
|
+
# manifest, and Rails 8's `rails new` boots the app (solid_cache:install, etc.)
|
|
56
|
+
# in `after_bundle` BEFORE `lcp_ruby:install` would create it — so the manifest
|
|
57
|
+
# must exist before any boot. `lcp_ruby:install`'s `ensure_sprockets_manifest!`
|
|
58
|
+
# is idempotent (only appends missing `//= link` lines), so this is the
|
|
59
|
+
# source-of-truth's early twin, not a fork.
|
|
60
|
+
unless skip_asset_pipeline
|
|
61
|
+
create_file "app/assets/config/manifest.js", <<~MANIFEST
|
|
62
|
+
//= link_directory ../stylesheets .css
|
|
63
|
+
//= link lcp_ruby/application.js
|
|
64
|
+
//= link lcp_ruby/application.css
|
|
65
|
+
//= link lcp_ruby/tom-select.css
|
|
66
|
+
//= link lcp_ruby/tom-select.complete.min.js
|
|
67
|
+
MANIFEST
|
|
68
|
+
end
|
|
69
|
+
|
|
38
70
|
# Generators that accept --format option. Source of truth lives in
|
|
39
71
|
# `LcpRuby::Generators::FeatureRegistry` once the gem is loaded
|
|
40
72
|
# (`after_bundle`); this list mirrors the wizard's curated set.
|
|
@@ -89,10 +121,14 @@ after_bundle do
|
|
|
89
121
|
# abort_on_failure behaviour (no `inline:`).
|
|
90
122
|
# Core install also sets up agent affordances at the end (skills +
|
|
91
123
|
# CLAUDE.md/AGENTS.md via `lcp_ruby:agent_setup`) — passive, no runtime impact.
|
|
92
|
-
|
|
124
|
+
install_args = [
|
|
93
125
|
"--format=#{format}",
|
|
94
126
|
"--menu-layout=#{menu_layout}",
|
|
95
127
|
"--locale=#{locale}"
|
|
128
|
+
]
|
|
129
|
+
install_args << "--skip-sample" if skip_sample
|
|
130
|
+
install_args << "--skip-asset-pipeline" if skip_asset_pipeline
|
|
131
|
+
generate "lcp_ruby:install", *install_args
|
|
96
132
|
|
|
97
133
|
# Build manifest header before the feature loop so even an empty preset
|
|
98
134
|
# produces a manifest the doctor can introspect.
|
data/lib/lcp_ruby/array_query.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
module LcpRuby
|
|
2
2
|
# DB-portable query helpers for array fields.
|
|
3
|
-
# Uses native PG array operators (@>, &&, <@) on PostgreSQL
|
|
4
|
-
# and
|
|
3
|
+
# Uses native PG array operators (@>, &&, <@) on PostgreSQL, a json_each()
|
|
4
|
+
# subquery on SQLite, and a JSON_TABLE() row source on MySQL/MariaDB (which
|
|
5
|
+
# has no json_each). Both non-PG backends expose each element as `je.value`,
|
|
6
|
+
# so the conditions below are shared.
|
|
5
7
|
class ArrayQuery
|
|
6
8
|
class << self
|
|
7
9
|
# Records where the array field contains ALL of the given values.
|
|
@@ -15,7 +17,7 @@ module LcpRuby
|
|
|
15
17
|
condition = if LcpRuby.postgresql?
|
|
16
18
|
"#{col} @> #{pg_array_literal(values, c, item_type)}"
|
|
17
19
|
else
|
|
18
|
-
"(SELECT COUNT(DISTINCT je.value) FROM
|
|
20
|
+
"(SELECT COUNT(DISTINCT je.value) FROM #{json_elements(col)} " \
|
|
19
21
|
"WHERE je.value IN (#{quoted_values(values, c)})) = #{values.size}"
|
|
20
22
|
end
|
|
21
23
|
|
|
@@ -33,7 +35,7 @@ module LcpRuby
|
|
|
33
35
|
condition = if LcpRuby.postgresql?
|
|
34
36
|
"#{col} && #{pg_array_literal(values, c, item_type)}"
|
|
35
37
|
else
|
|
36
|
-
"EXISTS (SELECT 1 FROM
|
|
38
|
+
"EXISTS (SELECT 1 FROM #{json_elements(col)} " \
|
|
37
39
|
"WHERE je.value IN (#{quoted_values(values, c)}))"
|
|
38
40
|
end
|
|
39
41
|
|
|
@@ -50,16 +52,14 @@ module LcpRuby
|
|
|
50
52
|
|
|
51
53
|
if values.empty?
|
|
52
54
|
return scope.where(Arel.sql(
|
|
53
|
-
LcpRuby.postgresql? ?
|
|
54
|
-
"#{col} = '{}'" :
|
|
55
|
-
"json_array_length(#{col}) = 0"
|
|
55
|
+
LcpRuby.postgresql? ? "#{col} = '{}'" : "#{json_length(col)} = 0"
|
|
56
56
|
))
|
|
57
57
|
end
|
|
58
58
|
|
|
59
59
|
condition = if LcpRuby.postgresql?
|
|
60
60
|
"#{col} <@ #{pg_array_literal(values, c, item_type)}"
|
|
61
61
|
else
|
|
62
|
-
"NOT EXISTS (SELECT 1 FROM
|
|
62
|
+
"NOT EXISTS (SELECT 1 FROM #{json_elements(col)} " \
|
|
63
63
|
"WHERE je.value NOT IN (#{quoted_values(values, c)}))"
|
|
64
64
|
end
|
|
65
65
|
|
|
@@ -74,7 +74,7 @@ module LcpRuby
|
|
|
74
74
|
if LcpRuby.postgresql?
|
|
75
75
|
"COALESCE(array_length(#{col}, 1), 0)"
|
|
76
76
|
else
|
|
77
|
-
|
|
77
|
+
json_length(col)
|
|
78
78
|
end
|
|
79
79
|
end
|
|
80
80
|
|
|
@@ -88,13 +88,36 @@ module LcpRuby
|
|
|
88
88
|
"EXISTS (SELECT 1 FROM unnest(#{col}) item " \
|
|
89
89
|
"WHERE item ILIKE #{quoted_query})"
|
|
90
90
|
else
|
|
91
|
-
"EXISTS (SELECT 1 FROM
|
|
91
|
+
"EXISTS (SELECT 1 FROM #{json_elements(col)} " \
|
|
92
92
|
"WHERE je.value LIKE #{quoted_query})"
|
|
93
93
|
end
|
|
94
94
|
end
|
|
95
95
|
|
|
96
96
|
private
|
|
97
97
|
|
|
98
|
+
# Row source aliased `je` exposing each array element as a TEXT `je.value`,
|
|
99
|
+
# so the conditions can compare it against the quoted string literals from
|
|
100
|
+
# quoted_values regardless of the element's JSON type.
|
|
101
|
+
#
|
|
102
|
+
# MySQL/MariaDB has no json_each — JSON_TABLE with a LONGTEXT column yields
|
|
103
|
+
# text already (LONGTEXT, not a sized VARCHAR, so elements > 255 chars such
|
|
104
|
+
# as URLs aren't silently truncated). SQLite's json_each preserves the JSON
|
|
105
|
+
# type, so a numeric element comes back as an INTEGER/REAL that never equals
|
|
106
|
+
# the string literal `'1'` — wrap it in a derived table that CASTs to TEXT
|
|
107
|
+
# (TEXT is not a valid MySQL CAST target, hence the per-adapter split).
|
|
108
|
+
def json_elements(col)
|
|
109
|
+
if LcpRuby.mysql?
|
|
110
|
+
"JSON_TABLE(#{col}, '$[*]' COLUMNS(value LONGTEXT PATH '$')) je"
|
|
111
|
+
else
|
|
112
|
+
"(SELECT CAST(value AS TEXT) AS value FROM json_each(#{col})) je"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Number of elements in the JSON array column.
|
|
117
|
+
def json_length(col)
|
|
118
|
+
LcpRuby.mysql? ? "JSON_LENGTH(#{col})" : "json_array_length(#{col})"
|
|
119
|
+
end
|
|
120
|
+
|
|
98
121
|
def connection
|
|
99
122
|
ActiveRecord::Base.connection
|
|
100
123
|
end
|
|
@@ -9,7 +9,7 @@ module LcpRuby
|
|
|
9
9
|
module CLI
|
|
10
10
|
class NewCommand
|
|
11
11
|
MINIMUM_RAILS_VERSION = "8.1"
|
|
12
|
-
MINIMUM_RUBY_VERSION = "3.
|
|
12
|
+
MINIMUM_RUBY_VERSION = "3.2"
|
|
13
13
|
|
|
14
14
|
DATABASES = %w[sqlite3 postgresql mysql2].freeze
|
|
15
15
|
|
|
@@ -134,7 +134,10 @@ module LcpRuby
|
|
|
134
134
|
end
|
|
135
135
|
|
|
136
136
|
def check_rails_version!
|
|
137
|
-
|
|
137
|
+
# File::NULL (not a literal /dev/null) so the stderr redirect is valid
|
|
138
|
+
# on Windows cmd.exe too — otherwise the shell-out fails and this reads
|
|
139
|
+
# as "rails not found", aborting `lcp new` on Windows.
|
|
140
|
+
version_output = `rails --version 2>#{File::NULL}`.strip
|
|
138
141
|
if version_output.empty?
|
|
139
142
|
abort "Error: 'rails' command not found. Install Rails #{MINIMUM_RAILS_VERSION}+:\n gem install rails"
|
|
140
143
|
end
|
|
@@ -387,7 +390,9 @@ module LcpRuby
|
|
|
387
390
|
"LCP_GEM_SOURCE" => @gem_source,
|
|
388
391
|
"LCP_TIMEZONE" => @timezone,
|
|
389
392
|
"LCP_LOCALE" => @locale,
|
|
390
|
-
"LCP_MENU_LAYOUT" => @menu_layout
|
|
393
|
+
"LCP_MENU_LAYOUT" => @menu_layout,
|
|
394
|
+
"LCP_SKIP_SAMPLE" => @options[:skip_sample] ? "1" : "0",
|
|
395
|
+
"LCP_SKIP_ASSET_PIPELINE" => @options[:skip_asset_pipeline] ? "1" : "0"
|
|
391
396
|
}
|
|
392
397
|
|
|
393
398
|
# Action Text is installed by default — `:rich_text` fields are a
|
|
@@ -436,7 +441,7 @@ module LcpRuby
|
|
|
436
441
|
say ""
|
|
437
442
|
say "Application created successfully!", :green
|
|
438
443
|
say ""
|
|
439
|
-
say "AI agents are set up: CLAUDE.md + .claude/skills/
|
|
444
|
+
say "AI agents are set up: CLAUDE.md + .claude/skills and .codex/skills are ready.", :green
|
|
440
445
|
say " Reference docs: bundle exec rake lcp_ruby:docs"
|
|
441
446
|
say " Validate config: bundle exec rake lcp_ruby:validate"
|
|
442
447
|
say ""
|