plutonium 0.45.3 → 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.
- checksums.yaml +4 -4
- data/.claude/skills/plutonium/SKILL.md +146 -0
- data/.claude/skills/plutonium-assets/SKILL.md +248 -157
- data/.claude/skills/{plutonium-rodauth → plutonium-auth}/SKILL.md +195 -229
- data/.claude/skills/plutonium-controller/SKILL.md +9 -2
- data/.claude/skills/plutonium-create-resource/SKILL.md +22 -1
- data/.claude/skills/plutonium-definition/SKILL.md +521 -7
- data/.claude/skills/plutonium-entity-scoping/SKILL.md +317 -0
- data/.claude/skills/plutonium-forms/SKILL.md +8 -1
- data/.claude/skills/plutonium-installation/SKILL.md +25 -2
- data/.claude/skills/plutonium-interaction/SKILL.md +9 -2
- data/.claude/skills/plutonium-invites/SKILL.md +11 -7
- data/.claude/skills/plutonium-model/SKILL.md +50 -50
- data/.claude/skills/plutonium-nested-resources/SKILL.md +8 -1
- data/.claude/skills/plutonium-package/SKILL.md +8 -1
- data/.claude/skills/plutonium-policy/SKILL.md +69 -78
- data/.claude/skills/plutonium-portal/SKILL.md +26 -70
- data/.claude/skills/plutonium-views/SKILL.md +9 -2
- data/CHANGELOG.md +28 -0
- data/app/assets/plutonium.css +1 -1
- data/app/views/rodauth/_login_form.html.erb +0 -3
- data/app/views/rodauth/confirm_password.html.erb +0 -4
- data/app/views/rodauth/create_account.html.erb +0 -3
- data/app/views/rodauth/logout.html.erb +0 -3
- data/docs/superpowers/plans/2026-04-08-plutonium-skills-overhaul.md +481 -0
- data/docs/superpowers/specs/2026-04-08-plutonium-skills-overhaul-design.md +236 -0
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/core/update/update_generator.rb +8 -0
- data/lib/generators/pu/gem/active_shrine/active_shrine_generator.rb +56 -0
- data/lib/generators/pu/invites/install_generator.rb +8 -1
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +43 -0
- data/lib/generators/pu/profile/concerns/profile_arguments.rb +10 -4
- data/lib/generators/pu/profile/conn_generator.rb +9 -12
- data/lib/generators/pu/profile/install_generator.rb +5 -2
- data/lib/generators/pu/rodauth/templates/app/rodauth/account_rodauth_plugin.rb.tt +3 -0
- data/lib/generators/pu/saas/portal_generator.rb +4 -9
- data/lib/generators/pu/saas/welcome/templates/app/views/welcome/onboarding.html.erb.tt +2 -2
- data/lib/plutonium/engine.rb +18 -5
- data/lib/plutonium/ui/layout/rodauth_layout.rb +6 -1
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- metadata +7 -8
- data/.claude/skills/plutonium/skill.md +0 -130
- data/.claude/skills/plutonium-definition-actions/SKILL.md +0 -424
- data/.claude/skills/plutonium-definition-query/SKILL.md +0 -364
- data/.claude/skills/plutonium-profile/SKILL.md +0 -276
- 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`.
|
|
@@ -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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
44
|
+
" has_one :profile, class_name: \"#{namespaced_class_name}\", dependent: :destroy\n"
|
|
42
45
|
else
|
|
43
|
-
" has_one
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
<%%=
|
|
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
|
-
<%%=
|
|
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,
|
data/lib/plutonium/engine.rb
CHANGED
|
@@ -5,17 +5,30 @@ module Plutonium
|
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
7
|
class_methods do
|
|
8
|
-
attr_reader :
|
|
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
|
-
@
|
|
16
|
+
@scoped_entity_class_name = entity_class.is_a?(Class) ? entity_class.name : entity_class.to_s
|
|
12
17
|
@scoped_entity_strategy = strategy
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
data/lib/plutonium/version.rb
CHANGED
data/package.json
CHANGED