plutonium 0.45.2 → 0.46.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium/SKILL.md +146 -0
  3. data/.claude/skills/plutonium-assets/SKILL.md +248 -157
  4. data/.claude/skills/{plutonium-rodauth → plutonium-auth}/SKILL.md +195 -229
  5. data/.claude/skills/plutonium-controller/SKILL.md +9 -2
  6. data/.claude/skills/plutonium-create-resource/SKILL.md +22 -1
  7. data/.claude/skills/plutonium-definition/SKILL.md +521 -7
  8. data/.claude/skills/plutonium-entity-scoping/SKILL.md +317 -0
  9. data/.claude/skills/plutonium-forms/SKILL.md +8 -1
  10. data/.claude/skills/plutonium-installation/SKILL.md +25 -2
  11. data/.claude/skills/plutonium-interaction/SKILL.md +9 -2
  12. data/.claude/skills/plutonium-invites/SKILL.md +11 -7
  13. data/.claude/skills/plutonium-model/SKILL.md +50 -50
  14. data/.claude/skills/plutonium-nested-resources/SKILL.md +8 -1
  15. data/.claude/skills/plutonium-package/SKILL.md +8 -1
  16. data/.claude/skills/plutonium-policy/SKILL.md +69 -78
  17. data/.claude/skills/plutonium-portal/SKILL.md +26 -70
  18. data/.claude/skills/plutonium-views/SKILL.md +9 -2
  19. data/CHANGELOG.md +33 -0
  20. data/app/assets/plutonium.css +1 -1
  21. data/app/views/rodauth/_login_form.html.erb +0 -3
  22. data/app/views/rodauth/confirm_password.html.erb +0 -4
  23. data/app/views/rodauth/create_account.html.erb +0 -3
  24. data/app/views/rodauth/logout.html.erb +0 -3
  25. data/config/initializers/pagy.rb +1 -1
  26. data/docs/superpowers/plans/2026-04-08-plutonium-skills-overhaul.md +481 -0
  27. data/docs/superpowers/specs/2026-04-08-plutonium-skills-overhaul-design.md +236 -0
  28. data/gemfiles/rails_7.gemfile.lock +1 -1
  29. data/gemfiles/rails_8.0.gemfile.lock +1 -1
  30. data/gemfiles/rails_8.1.gemfile.lock +1 -1
  31. data/lib/generators/pu/core/update/update_generator.rb +8 -0
  32. data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +56 -0
  33. data/lib/generators/pu/invites/install_generator.rb +8 -1
  34. data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +43 -0
  35. data/lib/generators/pu/profile/concerns/profile_arguments.rb +10 -4
  36. data/lib/generators/pu/profile/conn_generator.rb +9 -12
  37. data/lib/generators/pu/profile/install_generator.rb +5 -2
  38. data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +3 -0
  39. data/lib/generators/pu/saas/portal_generator.rb +4 -9
  40. data/lib/generators/pu/saas/welcome/templates/app/views/welcome/onboarding.html.erb.tt +2 -2
  41. data/lib/plutonium/engine.rb +18 -5
  42. data/lib/plutonium/ui/layout/rodauth_layout.rb +6 -1
  43. data/lib/plutonium/version.rb +1 -1
  44. data/package.json +1 -1
  45. metadata +7 -8
  46. data/.claude/skills/plutonium/skill.md +0 -130
  47. data/.claude/skills/plutonium-definition-actions/SKILL.md +0 -424
  48. data/.claude/skills/plutonium-definition-query/SKILL.md +0 -364
  49. data/.claude/skills/plutonium-profile/SKILL.md +0 -276
  50. data/.claude/skills/plutonium-theming/SKILL.md +0 -424
@@ -0,0 +1,236 @@
1
+ # Plutonium Skills Overhaul — Design
2
+
3
+ **Date:** 2026-04-08
4
+ **Status:** Approved (pending user review of this doc)
5
+
6
+ ## Problem
7
+
8
+ The current Plutonium skills (`.claude/skills/plutonium-*`) are content-correct but
9
+ fail to trigger at the right moments. Concrete failure modes observed in real
10
+ sessions:
11
+
12
+ 1. Agents write `relation_scope` overrides without ever loading `plutonium-policy`,
13
+ bypassing `default_relation_scope` and breaking entity scoping.
14
+ 2. Agents hand-write resource files instead of using `pu:res:scaffold`.
15
+ 3. Agents miss cross-cutting concerns (multi-tenancy spans 4 skills; nobody reads 4).
16
+ 4. Greenfield onboarding is one-skill-at-a-time as the agent stumbles into mistakes.
17
+ 5. The definition concept is split across 3 skills — agents read one and miss the others.
18
+
19
+ Root cause: skill `description:` fields list **topics** ("authorization, attribute
20
+ visibility, relation scoping..."). Agents triage descriptions against **moments**
21
+ ("I am about to write `relation_scope`"). Topic-based descriptions don't fire.
22
+
23
+ ## Goals
24
+
25
+ 1. Skills get invoked at the *moment* an agent is about to make a relevant decision.
26
+ 2. The most expensive mistakes live in a fixed 🚨 block at the top of the relevant skill.
27
+ 3. Cross-cutting concerns (entity scoping especially) are consolidated and discoverable.
28
+ 4. **Generators are the default path.** Every skill that touches a generator-backed
29
+ concern leads with "use `pu:...`, do not hand-write."
30
+ 5. **Greenfield onboarding works.** When an agent starts new work, it loads a bundle
31
+ of foundational skills upfront via the index skill.
32
+ 6. The index skill (`plutonium`) acts as a router AND a greenfield bootstrapper.
33
+ 7. Critical workflows are checklists; explanation is prose.
34
+
35
+ ## Non-goals
36
+
37
+ - Not rewriting prose content wholesale — content is mostly correct.
38
+ - No telemetry/measurement infrastructure.
39
+ - No new code examples beyond what's needed for the 🚨 blocks.
40
+ - No skill renames beyond `plutonium-rodauth → plutonium-auth`.
41
+
42
+ ## Final skill set (20 → 17)
43
+
44
+ ### Merges
45
+
46
+ | New skill | Absorbs | Rationale |
47
+ |---|---|---|
48
+ | `plutonium-definition` | + `plutonium-definition-actions`, `plutonium-definition-query` | Agents writing a definition need all three; today's split causes "read one, miss two" |
49
+ | `plutonium-auth` (renamed from `plutonium-rodauth`) | + `plutonium-profile` | Profile is a thin layer over rodauth, almost never edited independently |
50
+ | `plutonium-assets` | + `plutonium-theming` | Both are "configure the frontend toolchain" — Tailwind + Stimulus + tokens are one mental model |
51
+
52
+ ### New skill
53
+
54
+ | Skill | Purpose |
55
+ |---|---|
56
+ | `plutonium-entity-scoping` | Consolidates the entity-scoping/multi-tenancy-specific content currently fragmented across `plutonium-model`, `plutonium-policy`, `plutonium-portal`, and `plutonium-invites`. The single source of truth for: `associated_with` resolution, `default_relation_scope` rules, `relation_scope` override safety, entity strategies (path/custom), and the join-table/grandchild model shapes. |
57
+
58
+ The four source skills retain their general content but defer to
59
+ `plutonium-entity-scoping` for tenancy specifics via cross-reference.
60
+
61
+ ### Stays separate
62
+
63
+ `plutonium`, `plutonium-installation`, `plutonium-create-resource`, `plutonium-model`,
64
+ `plutonium-policy`, `plutonium-controller`, `plutonium-interaction`, `plutonium-portal`,
65
+ `plutonium-package`, `plutonium-nested-resources`, `plutonium-invites`,
66
+ `plutonium-forms`, `plutonium-views`.
67
+
68
+ **Final count: 17 skills** (20 − 3 merges + 1 new − 1 rename effect = 17).
69
+
70
+ ## Skill template
71
+
72
+ Every skill follows this fixed shape so agents always know where to look:
73
+
74
+ ```markdown
75
+ ---
76
+ name: plutonium-<topic>
77
+ description: Use BEFORE <specific verb/construct>. Also when <secondary trigger>. <one-line scope>.
78
+ ---
79
+
80
+ # plutonium-<topic>
81
+
82
+ ## 🚨 Critical (read first)
83
+ - **Use generators, not hand-written files.** `pu:<gen>` — never create <X> manually.
84
+ - **<Top anti-pattern #1>** — one-liner + why.
85
+ - **<Top anti-pattern #2>** — one-liner + why.
86
+ - **Related skills you may also need:** [list with one-line reasons]
87
+
88
+ ## When to use this skill
89
+ Checklist of decision points / code constructs that should trigger loading this skill.
90
+
91
+ ## Quick checklist (bootstrap + high-traffic skills only)
92
+ Numbered checklist for the most common workflow. Agent can TaskCreate from this.
93
+
94
+ ## <Sections — scaled to topic>
95
+ Prose + code examples.
96
+
97
+ ## Gotchas
98
+ The full anti-pattern list with explanations.
99
+
100
+ ## See also
101
+ Cross-references to related skills.
102
+ ```
103
+
104
+ ### Template rules
105
+
106
+ - **Description starts with `Use BEFORE <verb/construct>`** — verbs and code names,
107
+ not topic nouns. This is the single biggest triggering fix.
108
+ - **🚨 Critical block is fixed-position** (always right after the H1) and capped at
109
+ ~5 bullets.
110
+ - **Generator-first** is in the 🚨 block of every skill where a generator exists.
111
+ - **Cross-references in 🚨 are mandatory** — the "Related skills" bullet replaces
112
+ most of the multi-tenancy discoverability problem.
113
+ - **TOC at top of merged skills** so agents can jump to a section without reading
114
+ the whole file.
115
+
116
+ ## The index skill (`plutonium`) as router + bootstrapper
117
+
118
+ ```markdown
119
+ ---
120
+ name: plutonium
121
+ description: Use BEFORE starting any Plutonium work — new app, new feature, or first edit in an unfamiliar area. Routes you to the right skills and bootstraps greenfield work.
122
+ ---
123
+
124
+ # plutonium
125
+
126
+ ## 🚨 Read this first
127
+ - Plutonium is generator-driven. Almost every file you'd hand-write has a `pu:*`
128
+ generator. Use it. Hand-written files drift from conventions.
129
+ - For greenfield (new app or substantial new feature), load the **bootstrap bundle**
130
+ below before writing any code.
131
+ - For targeted edits, use the **router table**.
132
+ - For anything touching tenant scoping, load `plutonium-entity-scoping`.
133
+
134
+ ## Greenfield bootstrap bundle
135
+ Triggers: installing Plutonium, adding the first resource of a new domain, building
136
+ a new portal/package, "set up X from scratch", "build me a Y app".
137
+
138
+ Load ALL of these before writing code:
139
+ 1. `plutonium-installation`
140
+ 2. `plutonium-create-resource`
141
+ 3. `plutonium-model`
142
+ 4. `plutonium-policy`
143
+ 5. `plutonium-entity-scoping` ← new
144
+ 6. `plutonium-portal`
145
+ 7. `plutonium-definition`
146
+
147
+ ## Router (targeted edits)
148
+ | About to... | Load |
149
+ |---|---|
150
+ | Write/edit a model, add associations | `plutonium-model` |
151
+ | Scope a model to a tenant, write `associated_with`, deal with multi-tenancy | `plutonium-entity-scoping` |
152
+ | Write `relation_scope`, `permitted_attributes`, override a policy | `plutonium-policy` (+ `plutonium-entity-scoping` if scoping) |
153
+ | Add fields, search, filters, custom actions to a resource | `plutonium-definition` |
154
+ | Customize a controller action, hook, redirect | `plutonium-controller` |
155
+ | Encapsulate business logic, multi-step ops | `plutonium-interaction` |
156
+ | Build a custom page, panel, table, layout | `plutonium-views` |
157
+ | Customize forms, field builders, inputs | `plutonium-forms` |
158
+ | Configure Tailwind, Stimulus, design tokens | `plutonium-assets` |
159
+ | Set up Rodauth, accounts, profile pages | `plutonium-auth` |
160
+ | Set up user invitations / membership | `plutonium-invites` (+ `plutonium-entity-scoping`) |
161
+ | Configure parent/child resources, nested routes | `plutonium-nested-resources` |
162
+ | Create a portal or feature package | `plutonium-portal` / `plutonium-package` |
163
+
164
+ ## Generator catalog
165
+ [Table of `pu:*` generators with one-line purpose + which skill covers it.]
166
+ ```
167
+
168
+ ## Execution phases
169
+
170
+ ### Phase A — Restructure (mechanical)
171
+ 1. Merge `plutonium-definition-actions` + `plutonium-definition-query` into
172
+ `plutonium-definition` with TOC + sections.
173
+ 2. Rename `plutonium-rodauth` → `plutonium-auth`; merge `plutonium-profile` into it.
174
+ 3. Merge `plutonium-theming` into `plutonium-assets` with TOC + sections.
175
+ 4. Delete merged source skills.
176
+ 5. Grep the codebase for references to deleted/renamed skill names; update.
177
+
178
+ ### Phase B — Create `plutonium-entity-scoping`
179
+ - Extract entity-scoping content from `plutonium-model`, `plutonium-policy`,
180
+ `plutonium-portal`, `plutonium-invites`.
181
+ - Single source of truth for: `associated_with`, `default_relation_scope`,
182
+ `relation_scope` override safety, entity strategies, and the three model shapes
183
+ (direct child, join-table, grandchild) with worked examples.
184
+ - Source skills keep general content but link here for tenancy specifics.
185
+
186
+ ### Phase C — Rewrite descriptions (every skill)
187
+ - Format: `Use BEFORE <verb/construct>. Also when <secondary>. <scope>.`
188
+ - Each calls out specific code constructs.
189
+
190
+ ### Phase D — Add 🚨 Critical block to every skill
191
+ - Fixed position (right after H1).
192
+ - ~5 bullets max.
193
+ - Pull existing anti-patterns from gotchas to the top.
194
+
195
+ ### Phase E — Rewrite `plutonium` index skill
196
+ - Bootstrap bundle.
197
+ - Router table.
198
+ - Generator catalog.
199
+
200
+ ### Phase F — Quick checklists
201
+ Add to bootstrap-bundle skills + `plutonium-definition` + `plutonium-entity-scoping`.
202
+ Skip for low-traffic skills.
203
+
204
+ ### Phase G — Cross-references
205
+ - Every tenancy-touching skill links to `plutonium-entity-scoping`.
206
+ - Definition / policy / model get mutual cross-refs.
207
+ - Verify bidirectionality.
208
+
209
+ ## Risks & mitigations
210
+
211
+ | Risk | Mitigation |
212
+ |---|---|
213
+ | Merged skills are bigger → more tokens per load | TOC at top, section anchors, router tells agent which section to jump to |
214
+ | Renaming `plutonium-rodauth → plutonium-auth` breaks references | Grep + update in Phase A step 5; also update CLAUDE.md if mentioned |
215
+ | Description rewrites are subjective | Same pattern for all (`Use BEFORE <verb>`), reviewed for consistency |
216
+ | `plutonium-entity-scoping` could become a dumping ground | Strict scope: only entity scoping itself. General model/policy stuff stays in source skills. |
217
+ | Source skills' tenancy sections become stubs that drift | Rule: source skill has a 🚨 bullet "for entity scoping, see `plutonium-entity-scoping`" and a one-paragraph teaser, nothing more |
218
+
219
+ ## Out of scope
220
+
221
+ - New code examples beyond 🚨 blocks and the three model-shape examples in
222
+ `plutonium-entity-scoping`.
223
+ - Telemetry / measurement.
224
+ - Skill content rewrites beyond what's needed for the new structure.
225
+ - Consolidating below 17 skills.
226
+
227
+ ## Success criteria
228
+
229
+ 1. An agent about to write `relation_scope` loads `plutonium-policy` AND
230
+ `plutonium-entity-scoping` from description triggering alone.
231
+ 2. An agent doing greenfield work loads the bootstrap bundle from a single read of
232
+ `plutonium`.
233
+ 3. Every skill that has a generator mentions "use the generator" in its 🚨 block.
234
+ 4. The three model shapes (direct child / join table / grandchild) have worked
235
+ examples in `plutonium-entity-scoping`.
236
+ 5. No skill description starts with a topic noun list; all start with `Use BEFORE`.
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.45.0)
4
+ plutonium (0.45.3)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 43.0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.45.0)
4
+ plutonium (0.45.3)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 43.0)
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- plutonium (0.45.0)
4
+ plutonium (0.45.3)
5
5
  action_policy (~> 0.7.0)
6
6
  listen (~> 3.8)
7
7
  pagy (~> 43.0)
@@ -12,12 +12,20 @@ module Pu
12
12
  def start
13
13
  update_gem
14
14
  update_npm_package
15
+ sync_skills_if_present
15
16
  rescue => e
16
17
  exception "#{self.class} failed:", e
17
18
  end
18
19
 
19
20
  private
20
21
 
22
+ def sync_skills_if_present
23
+ return unless File.file?(Rails.root.join(".claude", "skills", "plutonium", "SKILL.md"))
24
+
25
+ say_status :update, "Syncing Plutonium Claude skills...", :green
26
+ Rails::Generators.invoke("pu:skills:sync", [], destination_root: Rails.root)
27
+ end
28
+
21
29
  def update_gem
22
30
  say_status :update, "Updating plutonium gem...", :green
23
31
  run "bundle update plutonium"
@@ -23,9 +23,65 @@ module Pu
23
23
 
24
24
  generate "active_shrine:install"
25
25
  template "config/initializers/shrine.rb", force: true
26
+
27
+ disable_active_storage_railtie
28
+ include_active_shrine_model_in_application_record
26
29
  rescue => e
27
30
  exception "#{self.class} failed:", e
28
31
  end
32
+
33
+ private
34
+
35
+ # Active Storage and active_shrine both define `has_one_attached`. Active
36
+ # Storage is loaded by `require "rails/all"`, so it wins by default and
37
+ # `has_one_attached :foo` quietly creates Active Storage attachments
38
+ # (which fail at runtime because the table doesn't exist). Replace
39
+ # `rails/all` with explicit framework requires that exclude
40
+ # active_storage/engine.
41
+ def disable_active_storage_railtie
42
+ return unless File.exist?("config/application.rb")
43
+ unless File.read("config/application.rb").include?(%(require "rails/all"))
44
+ say_status :warn,
45
+ "config/application.rb does not use `require \"rails/all\"`; skipping Active Storage railtie removal. " \
46
+ "Ensure active_storage/engine is NOT required, or `has_one_attached` will resolve to Active Storage instead of active_shrine.",
47
+ :yellow
48
+ return
49
+ end
50
+
51
+ gsub_file "config/application.rb",
52
+ /^require "rails\/all"$/,
53
+ <<~RUBY.strip
54
+ require "rails"
55
+ # Active Storage is intentionally excluded — file uploads use active_shrine.
56
+ %w[
57
+ active_record/railtie
58
+ active_model/railtie
59
+ active_job/railtie
60
+ action_controller/railtie
61
+ action_view/railtie
62
+ action_mailer/railtie
63
+ action_cable/engine
64
+ rails/test_unit/railtie
65
+ ].each { |railtie| require railtie }
66
+ RUBY
67
+
68
+ # Strip per-environment active_storage.service config since the railtie
69
+ # is gone.
70
+ Dir.glob("config/environments/*.rb").each do |env_file|
71
+ gsub_file env_file,
72
+ /^\s*config\.active_storage\.service\s*=.*\n/,
73
+ ""
74
+ end
75
+ end
76
+
77
+ # Include ActiveShrine::Model on ApplicationRecord so the gem's
78
+ # `has_one_attached` / `has_many_attached` macros are available everywhere.
79
+ def include_active_shrine_model_in_application_record
80
+ return unless File.exist?("app/models/application_record.rb")
81
+ return if File.read("app/models/application_record.rb").include?("ActiveShrine::Model")
82
+
83
+ inject_into_class "app/models/application_record.rb", "ApplicationRecord", " include ActiveShrine::Model\n"
84
+ end
29
85
  end
30
86
  end
31
87
  end
@@ -186,7 +186,7 @@ module Pu
186
186
  def current_membership
187
187
  return unless entity_scope && user
188
188
 
189
- @current_membership ||= #{membership_model}.find_by(#{entity_association_name}: entity_scope, user: user)
189
+ @current_membership ||= #{membership_model}.find_by(#{entity_association_name}: entity_scope, #{user_association_name}: user)
190
190
  end
191
191
  RUBY
192
192
 
@@ -382,6 +382,13 @@ module Pu
382
382
  PlutoniumGenerators::Generator.derive_association_name(membership_model, entity_model)
383
383
  end
384
384
 
385
+ # Returns the association name for user on the membership model.
386
+ # Same logic as entity_association_name but for the user side.
387
+ # e.g., RestaurantStaffUser -> StaffUser uses :staff_user (not :user)
388
+ def user_association_name
389
+ PlutoniumGenerators::Generator.derive_association_name(membership_model, user_model)
390
+ end
391
+
385
392
  def entity_in_package?
386
393
  options[:dest] != "main_app"
387
394
  end
@@ -472,6 +472,49 @@ module PlutoniumGenerators
472
472
  end
473
473
  end
474
474
 
475
+ #
476
+ # Injects helper methods into a Plutonium Concerns::Controller file,
477
+ # merging with any existing `included do` and `private` sections.
478
+ #
479
+ # ActiveSupport::Concern only permits ONE `included do` block per concern,
480
+ # so we cannot naively append a new block when multiple generators need to
481
+ # register helper_methods in the same file.
482
+ #
483
+ # @param file [String] path to the concerns/controller.rb file
484
+ # @param helper_methods [Array<Symbol>] names to expose via `helper_method`
485
+ # @param methods [String] method definitions (already indented to 6 spaces)
486
+ #
487
+ def inject_into_concerns_controller(file, helper_methods:, methods:)
488
+ helper_list = Array(helper_methods).map { |m| ":#{m}" }.join(", ")
489
+
490
+ in_root do
491
+ # 1. helper_method declaration: merge into existing `included do` block,
492
+ # otherwise create a new block right after the `# add concerns above.` marker.
493
+ content = File.read(file)
494
+ if (match = content.match(/^(?<indent>[ \t]*)included do\n/))
495
+ indent = match[:indent]
496
+ inject_into_file file,
497
+ "#{indent} helper_method #{helper_list}\n",
498
+ after: /^[ \t]*included do\n/
499
+ else
500
+ block = "\n included do\n helper_method #{helper_list}\n end\n"
501
+ inject_into_file file, block, after: /# add concerns above\.\n/
502
+ end
503
+
504
+ # 2. Method definitions: append into the existing `private` section if any,
505
+ # otherwise create one just before the closing ` end`s of the concern.
506
+ content = File.read(file)
507
+ trimmed_methods = methods.sub(/\A\n+/, "").chomp
508
+ if content.match?(/^[ \t]*private\n/)
509
+ inject_into_file file, "\n#{trimmed_methods}\n", after: /^[ \t]*private\n/
510
+ else
511
+ inject_into_file file,
512
+ "\n private\n\n#{trimmed_methods}\n",
513
+ before: /^ end\n end\nend\s*\z/
514
+ end
515
+ end
516
+ end
517
+
475
518
  def file_includes?(path, check)
476
519
  destination = File.expand_path(path, destination_root)
477
520
  return false unless File.exist?(destination)
@@ -7,15 +7,21 @@ module Pu
7
7
  extend ActiveSupport::Concern
8
8
 
9
9
  included do
10
- argument :name, type: :string, default: "Profile", required: false, banner: "NAME"
10
+ argument :name, type: :string, required: false, banner: "NAME"
11
11
  argument :attributes, type: :array, default: [], banner: "field[:type] field[:type]"
12
12
  end
13
13
 
14
- # Normalize arguments: if name contains ":", treat it as an attribute
14
+ # Normalize arguments: if name is omitted, default to "{UserModel}Profile";
15
+ # if name looks like an attribute (contains ":"), treat it as an attribute
16
+ # and still default the profile name to "{UserModel}Profile".
15
17
  def normalize_arguments
16
- if name.include?(":")
18
+ default_name = "#{options[:user_model] || "User"}Profile"
19
+ if name.nil?
20
+ @profile_name = default_name
21
+ @profile_attributes = attributes
22
+ elsif name.include?(":")
17
23
  @profile_attributes = [name, *attributes]
18
- @profile_name = "Profile"
24
+ @profile_name = default_name
19
25
  else
20
26
  @profile_name = name
21
27
  @profile_attributes = attributes
@@ -10,7 +10,7 @@ module Pu
10
10
 
11
11
  desc "Connect a Profile resource to a portal and configure the profile_url helper"
12
12
 
13
- argument :name, type: :string, default: "Profile", required: false, banner: "RESOURCE"
13
+ argument :name, type: :string, required: false, banner: "RESOURCE"
14
14
 
15
15
  class_option :dest, type: :string,
16
16
  desc: "Destination portal"
@@ -106,14 +106,7 @@ module Pu
106
106
  end
107
107
 
108
108
  def add_profile_url_helper
109
- content = <<-RUBY.chomp
110
-
111
- included do
112
- helper_method :profile_url
113
- end
114
-
115
- private
116
-
109
+ methods = <<-RUBY
117
110
  # Returns the URL to the user's profile page.
118
111
  def profile_url
119
112
  profile = current_user.#{profile_association}
@@ -124,15 +117,19 @@ module Pu
124
117
  end
125
118
  end
126
119
  RUBY
127
- inject_into_file concerns_controller_path, content, after: /# add concerns above\.\n/
120
+ inject_into_concerns_controller concerns_controller_path,
121
+ helper_methods: [:profile_url],
122
+ methods: methods
128
123
  end
129
124
 
130
125
  def profile_association
131
- resource_class_name.demodulize.underscore
126
+ # The install generator always exposes the profile as `:profile` on the
127
+ # user model (via class_name:), regardless of the underlying class name.
128
+ "profile"
132
129
  end
133
130
 
134
131
  def resource_class_name
135
- name.camelize
132
+ (name.presence || "#{options[:user_model]}Profile").camelize
136
133
  end
137
134
 
138
135
  def user_table
@@ -37,10 +37,13 @@ module Pu
37
37
  end
38
38
 
39
39
  def add_user_association
40
+ # Always expose the association as `:profile` on the user model so that
41
+ # `current_user.profile` works regardless of the underlying class name
42
+ # (e.g. UserProfile, StaffUserProfile, AccountSettings).
40
43
  association = if dest_package?
41
- " has_one :#{file_name}, class_name: \"#{namespaced_class_name}\", dependent: :destroy\n"
44
+ " has_one :profile, class_name: \"#{namespaced_class_name}\", dependent: :destroy\n"
42
45
  else
43
- " has_one :#{file_name}, dependent: :destroy\n"
46
+ " has_one :profile, class_name: \"#{class_name}\", dependent: :destroy\n"
44
47
  end
45
48
  inject_into_file user_model_path, association,
46
49
  before: /^\s*# add has_one associations above\.\n/
@@ -285,6 +285,9 @@ class <%= account_path.classify %>RodauthPlugin < RodauthPlugin
285
285
  <% end -%>
286
286
  <% if verify_account? -%>
287
287
 
288
+ # Redirect to login page after requesting account verification email.
289
+ verify_account_email_sent_redirect { login_path }
290
+
288
291
  # Redirect to wherever login redirects to after account verification.
289
292
  verify_account_redirect { login_redirect }
290
293
  <% end -%>
@@ -70,14 +70,7 @@ module Pu
70
70
  end
71
71
 
72
72
  def add_entity_url_helper
73
- content = <<-RUBY
74
-
75
- included do
76
- helper_method :entity_url, :user_entities
77
- end
78
-
79
- private
80
-
73
+ methods = <<-RUBY
81
74
  # Returns the URL to the current entity's show page.
82
75
  def entity_url
83
76
  resource_url_for(current_scoped_entity)
@@ -88,7 +81,9 @@ module Pu
88
81
  @user_entities ||= current_user.#{entity_table.pluralize}
89
82
  end
90
83
  RUBY
91
- inject_into_file concerns_controller_path, content, after: /# add concerns above\.\n/
84
+ inject_into_concerns_controller concerns_controller_path,
85
+ helper_methods: [:entity_url, :user_entities],
86
+ methods: methods
92
87
  end
93
88
 
94
89
  def add_entity_link_to_header
@@ -23,7 +23,7 @@
23
23
  <%% end %>
24
24
  <% if profile? -%>
25
25
 
26
- <%%= f.fields_for :profile, @profile do |pf| %>
26
+ <%%= fields_for :profile, @profile do |pf| %>
27
27
  <div>
28
28
  <%%= pf.label :name, "Your Name", class: "block mb-2 text-sm font-medium text-[var(--pu-text)]" %>
29
29
  <%%= pf.text_field :name,
@@ -34,7 +34,7 @@
34
34
  <%% end %>
35
35
  <% end -%>
36
36
 
37
- <%%= f.fields_for :<%= entity_table %>, @<%= entity_table %> do |ef| %>
37
+ <%%= fields_for :<%= entity_table %>, @<%= entity_table %> do |ef| %>
38
38
  <div>
39
39
  <%%= ef.label :name, "Workspace Name", class: "block mb-2 text-sm font-medium text-[var(--pu-text)]" %>
40
40
  <%%= ef.text_field :name,
@@ -5,17 +5,30 @@ module Plutonium
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  class_methods do
8
- attr_reader :scoped_entity_class, :scoped_entity_strategy, :scoped_entity_param_key, :scoped_entity_route_key
8
+ attr_reader :scoped_entity_strategy, :scoped_entity_param_key, :scoped_entity_route_key
9
9
 
10
+ # Store the entity class *by name* and resolve it lazily on every call.
11
+ # Capturing the class object directly causes stale references after Rails
12
+ # autoreload: the constant is reloaded but @scoped_entity_class still
13
+ # points at the previous (now-orphaned) class object, which then fails
14
+ # type checks against freshly reloaded instances.
10
15
  def scope_to_entity(entity_class, strategy: :path, param_key: nil, route_key: nil)
11
- @scoped_entity_class = entity_class
16
+ @scoped_entity_class_name = entity_class.is_a?(Class) ? entity_class.name : entity_class.to_s
12
17
  @scoped_entity_strategy = strategy
13
- @scoped_entity_param_key = param_key || :"#{entity_class.model_name.singular_route_key}_scope"
14
- @scoped_entity_route_key = route_key || entity_class.model_name.singular.to_sym
18
+ # param_key / route_key are derived from the class once at declaration
19
+ # time they're stable strings and don't depend on the live class
20
+ # identity, so caching them is safe.
21
+ resolved = @scoped_entity_class_name.constantize
22
+ @scoped_entity_param_key = param_key || :"#{resolved.model_name.singular_route_key}_scope"
23
+ @scoped_entity_route_key = route_key || resolved.model_name.singular.to_sym
24
+ end
25
+
26
+ def scoped_entity_class
27
+ @scoped_entity_class_name&.constantize
15
28
  end
16
29
 
17
30
  def scoped_to_entity?
18
- scoped_entity_class.present?
31
+ @scoped_entity_class_name.present?
19
32
  end
20
33
 
21
34
  def dom_id
@@ -19,7 +19,12 @@ module Plutonium
19
19
  render_logo
20
20
 
21
21
  div(class: "w-full bg-[var(--pu-surface)] rounded-[var(--pu-radius-lg)] border border-[var(--pu-border)] md:mt-0 sm:max-w-md xl:p-0", style: "box-shadow: var(--pu-shadow-md)") {
22
- div(class: "p-6 space-y-4 md:space-y-6 sm:p-8", &)
22
+ div(class: "p-6 space-y-4 md:space-y-6 sm:p-8") {
23
+ if page_title.present?
24
+ h1(class: "text-xl font-semibold leading-tight tracking-tight text-[var(--pu-text)] md:text-2xl") { page_title }
25
+ end
26
+ yield
27
+ }
23
28
  }
24
29
 
25
30
  render_links
@@ -1,5 +1,5 @@
1
1
  module Plutonium
2
- VERSION = "0.45.2"
2
+ VERSION = "0.46.0"
3
3
  NEXT_MAJOR_VERSION = VERSION.split(".").tap { |v|
4
4
  v[1] = v[1].to_i + 1
5
5
  v[2] = 0
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@radioactive-labs/plutonium",
3
- "version": "0.45.2",
3
+ "version": "0.46.0",
4
4
  "description": "Build production-ready Rails apps in minutes, not days. Convention-driven, fully customizable, AI-ready.",
5
5
  "type": "module",
6
6
  "main": "src/js/core.js",