ima-claude 2.20.0 → 2.25.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.
- package/README.md +48 -9
- package/dist/cli.js +1 -1
- package/package.json +1 -1
- package/plugins/ima-claude/.claude-plugin/plugin.json +1 -1
- package/plugins/ima-claude/agents/explorer.md +29 -15
- package/plugins/ima-claude/agents/implementer.md +58 -13
- package/plugins/ima-claude/agents/memory.md +19 -19
- package/plugins/ima-claude/agents/reviewer.md +56 -34
- package/plugins/ima-claude/agents/tester.md +59 -16
- package/plugins/ima-claude/agents/wp-developer.md +66 -21
- package/plugins/ima-claude/hooks/bootstrap.sh +42 -44
- package/plugins/ima-claude/hooks/prompt_coach_digest.md +14 -17
- package/plugins/ima-claude/hooks/prompt_coach_system.md +10 -12
- package/plugins/ima-claude/personalities/README.md +17 -6
- package/plugins/ima-claude/personalities/enable-efficient.md +61 -0
- package/plugins/ima-claude/personalities/enable-terse.md +71 -0
- package/plugins/ima-claude/skills/agentic-workflows/SKILL.md +35 -71
- package/plugins/ima-claude/skills/architect/SKILL.md +54 -168
- package/plugins/ima-claude/skills/compound-bridge/SKILL.md +41 -94
- package/plugins/ima-claude/skills/design-to-code/SKILL.md +43 -78
- package/plugins/ima-claude/skills/discourse/SKILL.md +79 -194
- package/plugins/ima-claude/skills/discourse-admin/SKILL.md +41 -103
- package/plugins/ima-claude/skills/docs-organize/SKILL.md +63 -203
- package/plugins/ima-claude/skills/ember-discourse/SKILL.md +90 -200
- package/plugins/ima-claude/skills/espocrm/SKILL.md +14 -23
- package/plugins/ima-claude/skills/espocrm-api/SKILL.md +79 -192
- package/plugins/ima-claude/skills/functional-programmer/SKILL.md +33 -237
- package/plugins/ima-claude/skills/gh-cli/SKILL.md +26 -65
- package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +71 -104
- package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +32 -22
- package/plugins/ima-claude/skills/ima-brand/SKILL.md +18 -23
- package/plugins/ima-claude/skills/ima-copywriting/SKILL.md +68 -179
- package/plugins/ima-claude/skills/ima-doc2pdf/SKILL.md +32 -102
- package/plugins/ima-claude/skills/ima-editorial-scorecard/SKILL.md +38 -63
- package/plugins/ima-claude/skills/ima-editorial-workflow/SKILL.md +69 -114
- package/plugins/ima-claude/skills/ima-email-creator/SKILL.md +16 -22
- package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +21 -37
- package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +39 -120
- package/plugins/ima-claude/skills/jquery/SKILL.md +107 -233
- package/plugins/ima-claude/skills/js-fp/SKILL.md +75 -296
- package/plugins/ima-claude/skills/js-fp-api/SKILL.md +52 -162
- package/plugins/ima-claude/skills/js-fp-react/SKILL.md +47 -270
- package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +55 -209
- package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +59 -204
- package/plugins/ima-claude/skills/livecanvas/SKILL.md +19 -32
- package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +92 -162
- package/plugins/ima-claude/skills/mcp-context7/SKILL.md +32 -64
- package/plugins/ima-claude/skills/mcp-gitea/SKILL.md +98 -188
- package/plugins/ima-claude/skills/mcp-github/SKILL.md +60 -124
- package/plugins/ima-claude/skills/mcp-memory/SKILL.md +1 -177
- package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +58 -115
- package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +32 -87
- package/plugins/ima-claude/skills/mcp-serena/SKILL.md +54 -80
- package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +40 -63
- package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +75 -116
- package/plugins/ima-claude/skills/php-authnet/SKILL.md +32 -65
- package/plugins/ima-claude/skills/php-fp/SKILL.md +50 -129
- package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +25 -73
- package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +103 -463
- package/plugins/ima-claude/skills/playwright/SKILL.md +69 -220
- package/plugins/ima-claude/skills/prompt-starter/SKILL.md +33 -83
- package/plugins/ima-claude/skills/prompt-starter/references/code-review.md +38 -0
- package/plugins/ima-claude/skills/py-fp/SKILL.md +78 -384
- package/plugins/ima-claude/skills/quasar-fp/SKILL.md +54 -255
- package/plugins/ima-claude/skills/quickstart/SKILL.md +7 -11
- package/plugins/ima-claude/skills/rails/SKILL.md +63 -184
- package/plugins/ima-claude/skills/resume-session/SKILL.md +14 -35
- package/plugins/ima-claude/skills/rg/SKILL.md +61 -146
- package/plugins/ima-claude/skills/ruby-fp/SKILL.md +66 -163
- package/plugins/ima-claude/skills/save-session/SKILL.md +10 -39
- package/plugins/ima-claude/skills/scorecard/SKILL.md +24 -38
- package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +42 -71
- package/plugins/ima-claude/skills/skill-creator/SKILL.md +79 -250
- package/plugins/ima-claude/skills/task-master/SKILL.md +11 -31
- package/plugins/ima-claude/skills/task-planner/SKILL.md +44 -153
- package/plugins/ima-claude/skills/task-runner/SKILL.md +61 -143
- package/plugins/ima-claude/skills/unit-testing/SKILL.md +59 -134
- package/plugins/ima-claude/skills/wp-ddev/SKILL.md +38 -120
- package/plugins/ima-claude/skills/wp-local/SKILL.md +26 -108
|
@@ -5,122 +5,87 @@ description: "Convert design screenshots into working WordPress code through a t
|
|
|
5
5
|
|
|
6
6
|
# Design to Code
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
You are the orchestrator. You analyze designs, compose prompts, delegate implementation, and verify results. You do NOT implement directly — you delegate to `ima-claude:wp-developer` agents.
|
|
11
|
-
|
|
12
|
-
---
|
|
8
|
+
Two-phase workflow: screenshots → implementation prompt (Phase A) → working WordPress code (Phase B). You orchestrate; delegate implementation to `ima-claude:wp-developer`.
|
|
13
9
|
|
|
14
10
|
## Mode Selection
|
|
15
11
|
|
|
16
|
-
Determine the mode from the user's input:
|
|
17
|
-
|
|
18
12
|
```
|
|
19
13
|
What did the user provide?
|
|
20
14
|
├── Screenshots/mockups (no existing prompt)
|
|
21
|
-
│ → Phase A
|
|
22
|
-
|
|
23
|
-
│
|
|
24
|
-
├── An existing implementation prompt
|
|
25
|
-
│ → Phase B: Prompt → Code
|
|
26
|
-
│ → Read references/phase-b-prompt-to-code.md
|
|
27
|
-
│
|
|
15
|
+
│ → Phase A → references/phase-a-design-to-prompt.md
|
|
16
|
+
├── Existing implementation prompt
|
|
17
|
+
│ → Phase B → references/phase-b-prompt-to-code.md
|
|
28
18
|
└── Screenshots + "implement this" / "full pipeline"
|
|
29
19
|
→ Phase A then Phase B in sequence
|
|
30
|
-
→ Phase A produces prompt → user reviews → Phase B executes
|
|
31
20
|
```
|
|
32
21
|
|
|
33
|
-
---
|
|
34
|
-
|
|
35
22
|
## Required Skills
|
|
36
23
|
|
|
37
|
-
|
|
38
|
-
- **`ima-
|
|
39
|
-
- **`
|
|
40
|
-
|
|
41
|
-
Phase B additionally needs:
|
|
42
|
-
- **`php-fp-wordpress`** — WordPress development patterns, security, shortcodes
|
|
43
|
-
|
|
44
|
-
---
|
|
24
|
+
- **`ima-brand`** — color palette, typography, mixins (both phases)
|
|
25
|
+
- **`ima-bootstrap`** — utility classes, grid, components
|
|
26
|
+
- **`php-fp-wordpress`** — WordPress patterns (Phase B only)
|
|
45
27
|
|
|
46
28
|
## Phase A: Design → Prompt
|
|
47
29
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
Before starting, search Qdrant for `design-to-prompt` to recall prior lessons.
|
|
51
|
-
|
|
52
|
-
### Steps (read `references/phase-a-design-to-prompt.md` for detailed procedures)
|
|
53
|
-
|
|
54
|
-
1. **GATHER** — Fetch Jira context + receive screenshots + explore codebase (parallel)
|
|
55
|
-
2. **ANALYZE** — Load brand palette from `ima-brand` (**must complete before COMPOSE**)
|
|
56
|
-
3. **CROP** — Full view → section detection → detail crops (iterative PIL cropping)
|
|
57
|
-
4. **EXTRACT** — Per crop: exact text, icons, colors, layout, spacing
|
|
58
|
-
5. **MAP** — Visual elements → brand variables, components → existing shortcodes
|
|
59
|
-
6. **COMPOSE** — Write prompt using `references/prompt-template.md` structure
|
|
60
|
-
7. **VALIDATE** — Re-check each section against its crop for accuracy
|
|
30
|
+
Output: ~200-300 line prompt file matching team template. Search Qdrant for `design-to-prompt` before starting.
|
|
61
31
|
|
|
62
|
-
|
|
32
|
+
| Step | Action |
|
|
33
|
+
|------|--------|
|
|
34
|
+
| GATHER | Fetch Jira context + receive screenshots + explore codebase (parallel) |
|
|
35
|
+
| ANALYZE | Load brand palette from `ima-brand` — must complete before COMPOSE |
|
|
36
|
+
| CROP | Full view → section detection → detail crops (iterative PIL cropping) |
|
|
37
|
+
| EXTRACT | Per crop: exact text, icons, colors, layout, spacing |
|
|
38
|
+
| MAP | Visual elements → brand variables, components → existing shortcodes |
|
|
39
|
+
| COMPOSE | Write prompt using `references/prompt-template.md` structure |
|
|
40
|
+
| VALIDATE | Re-check each section against its crop for accuracy |
|
|
63
41
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
---
|
|
42
|
+
Save prompt to `docs/designs/{ticket}/PROMPT.md` and Serena memory as `{feature-name}-plan`. Present to user; stop here unless running full pipeline.
|
|
67
43
|
|
|
68
44
|
## Phase B: Prompt → Code
|
|
69
45
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
Before starting, search Qdrant for `design-to-code` to recall prior lessons.
|
|
46
|
+
Search Qdrant for `design-to-code` before starting.
|
|
73
47
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
---
|
|
48
|
+
| Step | Action |
|
|
49
|
+
|------|--------|
|
|
50
|
+
| RESEARCH | Brand SCSS files + current code + component libraries (parallel explorers) |
|
|
51
|
+
| ARCHITECTURE | New file vs modify, function reuse, component migration decision |
|
|
52
|
+
| DECOMPOSE | Stories by page section; Story 1 = foundation, Stories 2-N = parallel fills, final = polish |
|
|
53
|
+
| IMPLEMENT | Delegate to `ima-claude:wp-developer` per story with precise prompts |
|
|
54
|
+
| REVIEW | Verify copy, colors, element order, asset paths before visual test |
|
|
55
|
+
| VISUAL-QA | Compile SASS → screenshot desktop + mobile → compare to design → iterate |
|
|
84
56
|
|
|
85
57
|
## Critical Guardrails
|
|
86
58
|
|
|
87
|
-
|
|
59
|
+
Full set in `references/guardrails.md`. Top 5:
|
|
88
60
|
|
|
89
|
-
1.
|
|
90
|
-
2.
|
|
91
|
-
3.
|
|
92
|
-
4.
|
|
93
|
-
5.
|
|
61
|
+
1. Never hardcode colors — use brand SCSS variables or Bootstrap utilities
|
|
62
|
+
2. Always verify asset paths exist — Glob/grep before referencing
|
|
63
|
+
3. Always provide exact copy text — include verbatim text in quotes, never let agents paraphrase
|
|
64
|
+
4. Load brand palette BEFORE composition — informs every color reference on first pass
|
|
65
|
+
5. Check site header/footer first — don't build components that duplicate existing site elements
|
|
94
66
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
## Agent Delegation Model
|
|
67
|
+
## Agent Delegation
|
|
98
68
|
|
|
99
69
|
| Role | Agent | When |
|
|
100
|
-
|
|
70
|
+
|------|-------|------|
|
|
101
71
|
| Orchestrator | opus (you) | All phases — research, planning, decomposition, delegation, review, surgical fixes |
|
|
102
72
|
| Codebase explorer | `ima-claude:explorer` (haiku) | GATHER/RESEARCH: find existing shortcodes, templates, SCSS files |
|
|
103
73
|
| Implementer | `ima-claude:wp-developer` (sonnet) | IMPLEMENT: write PHP/SCSS with skills: ima-brand, ima-bootstrap, php-fp-wordpress |
|
|
104
|
-
| Reviewer | `ima-claude:reviewer` (sonnet, read-only) | REVIEW: brand compliance + accessibility audit (
|
|
105
|
-
|
|
106
|
-
Orchestrator does surgical fixes (<5 lines) directly via Edit tool. Anything larger → delegate to wp-developer.
|
|
74
|
+
| Reviewer | `ima-claude:reviewer` (sonnet, read-only) | REVIEW: brand compliance + accessibility audit (larger implementations) |
|
|
107
75
|
|
|
108
|
-
|
|
76
|
+
Orchestrator does surgical fixes (<5 lines) directly via Edit. Anything larger → delegate to wp-developer.
|
|
109
77
|
|
|
110
78
|
## Qdrant Integration
|
|
111
79
|
|
|
112
|
-
|
|
113
|
-
- Phase
|
|
114
|
-
- Phase B: `qdrant_find("design-to-code implementation")` — retrieves decomposition patterns, delegation templates, QA patterns
|
|
115
|
-
|
|
116
|
-
---
|
|
80
|
+
- Phase A: `qdrant_find("design-to-prompt workflow")`
|
|
81
|
+
- Phase B: `qdrant_find("design-to-code implementation")`
|
|
117
82
|
|
|
118
83
|
## Related Skills
|
|
119
84
|
|
|
120
85
|
| Skill | Relationship |
|
|
121
|
-
|
|
86
|
+
|-------|------|
|
|
122
87
|
| `ima-brand` | Required — color palette, typography, mixins |
|
|
123
88
|
| `ima-bootstrap` | Required — utility classes, grid, components |
|
|
124
|
-
| `php-fp-wordpress` | Required for Phase B
|
|
125
|
-
| `task-master` | Optional —
|
|
126
|
-
| `prompt-starter` |
|
|
89
|
+
| `php-fp-wordpress` | Required for Phase B |
|
|
90
|
+
| `task-master` | Optional — complex multi-page designs needing Epic > Story > Task |
|
|
91
|
+
| `prompt-starter` | Phase A follows its "builder not executor" philosophy |
|
|
@@ -5,59 +5,38 @@ description: "Discourse plugin development - plugin.rb, after_initialize, admin
|
|
|
5
5
|
|
|
6
6
|
# Discourse Plugin Development
|
|
7
7
|
|
|
8
|
-
Discourse plugins are Rails engines
|
|
8
|
+
Discourse plugins are Rails engines. Work with the framework — Plugin API, Guardian, and event hooks exist for good reason.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Core Rules
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
12
|
+
- `after_initialize` — all plugin wiring goes here
|
|
13
|
+
- `Guardian` — authorization layer; every user action checks it
|
|
14
|
+
- `register_*` / `add_*` — use Plugin API hooks, not monkey-patches
|
|
15
|
+
- `StaffConstraint` — required on all admin routes; never roll your own
|
|
16
|
+
- `RuboCop` — enforced; Discourse ships lint rules
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
> Discourse plugins are Rails. Discourse adds its own authorization layer (Guardian). Respect both.
|
|
21
|
-
|
|
22
|
-
- **`after_initialize`** is where plugin logic wires into Discourse
|
|
23
|
-
- **Guardian** is the authorization layer — every action checks it
|
|
24
|
-
- **Plugin API** hooks (`register_*`, `add_*`) are the extension points — don't monkey-patch core
|
|
25
|
-
- **StaffConstraint** protects admin routes — use it, never roll your own
|
|
26
|
-
- **RuboCop** is enforced — Discourse ships lint rules
|
|
27
|
-
|
|
28
|
-
**Foundation**: Reference `../rails/SKILL.md` for Rails security practices and `../ruby-fp/SKILL.md` for Ruby patterns.
|
|
18
|
+
Foundation: `../rails/SKILL.md` (security), `../ruby-fp/SKILL.md` (patterns).
|
|
29
19
|
|
|
30
20
|
## Plugin Structure
|
|
31
21
|
|
|
32
22
|
```
|
|
33
23
|
my-plugin/
|
|
34
24
|
├── plugin.rb # Manifest + bootstrap (required)
|
|
35
|
-
├── about.json
|
|
25
|
+
├── about.json
|
|
36
26
|
├── app/
|
|
37
|
-
│ ├── controllers/
|
|
38
|
-
│
|
|
39
|
-
│
|
|
40
|
-
│ ├── models/
|
|
41
|
-
│ │ └── my_plugin_record.rb
|
|
42
|
-
│ └── serializers/
|
|
43
|
-
│ └── my_plugin_serializer.rb
|
|
27
|
+
│ ├── controllers/admin/my_plugin_controller.rb
|
|
28
|
+
│ ├── models/my_plugin_record.rb
|
|
29
|
+
│ └── serializers/my_plugin_serializer.rb
|
|
44
30
|
├── config/
|
|
45
|
-
│ ├── locales/
|
|
46
|
-
│
|
|
47
|
-
|
|
48
|
-
├──
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
├── assets/
|
|
52
|
-
│ └── javascripts/
|
|
53
|
-
│ └── admin/ # Ember components for admin UI
|
|
54
|
-
├── lib/
|
|
55
|
-
│ └── my_plugin/ # Pure logic (no Discourse deps)
|
|
56
|
-
└── spec/
|
|
57
|
-
└── plugin_helper.rb
|
|
31
|
+
│ ├── locales/server.en.yml
|
|
32
|
+
│ └── settings.yml
|
|
33
|
+
├── db/migrate/
|
|
34
|
+
├── assets/javascripts/admin/ # Ember admin UI
|
|
35
|
+
├── lib/my_plugin/ # Pure logic (no Discourse deps)
|
|
36
|
+
└── spec/plugin_helper.rb
|
|
58
37
|
```
|
|
59
38
|
|
|
60
|
-
## plugin.rb
|
|
39
|
+
## plugin.rb
|
|
61
40
|
|
|
62
41
|
```ruby
|
|
63
42
|
# frozen_string_literal: true
|
|
@@ -68,63 +47,44 @@ my-plugin/
|
|
|
68
47
|
# authors: Your Name
|
|
69
48
|
# url: https://github.com/yourorg/my-plugin
|
|
70
49
|
|
|
71
|
-
# Autoloading — define module first, then require engine
|
|
72
50
|
module ::MyPlugin
|
|
73
51
|
PLUGIN_NAME = "my-plugin"
|
|
74
52
|
end
|
|
75
53
|
|
|
76
54
|
require_relative "lib/my_plugin/engine"
|
|
77
55
|
|
|
78
|
-
# All wiring happens inside after_initialize
|
|
79
56
|
after_initialize do
|
|
80
|
-
#
|
|
81
|
-
# This runs after Discourse core is fully loaded
|
|
57
|
+
# All wiring here — runs after Discourse core is fully loaded
|
|
82
58
|
end
|
|
83
59
|
```
|
|
84
60
|
|
|
85
|
-
## after_initialize
|
|
61
|
+
## after_initialize
|
|
86
62
|
|
|
87
63
|
```ruby
|
|
88
64
|
after_initialize do
|
|
89
|
-
# Extend models — use class_eval in after_initialize, not top-level monkey-patches
|
|
90
65
|
User.class_eval do
|
|
91
66
|
has_one :my_plugin_profile, dependent: :destroy
|
|
92
67
|
end
|
|
93
68
|
|
|
94
|
-
# Register custom fields
|
|
95
69
|
register_post_custom_field_type('wp_original_id', :integer)
|
|
96
70
|
register_topic_custom_field_type('imported_from', :string)
|
|
97
71
|
|
|
98
|
-
|
|
99
|
-
on(:
|
|
100
|
-
MyPlugin::UserSetup.call(user)
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
on(:post_created) do |post, opts, user|
|
|
104
|
-
MyPlugin::PostSync.call(post, user)
|
|
105
|
-
end
|
|
72
|
+
on(:user_created) { |user| MyPlugin::UserSetup.call(user) }
|
|
73
|
+
on(:post_created) { |post, opts, user| MyPlugin::PostSync.call(post, user) }
|
|
106
74
|
|
|
107
|
-
|
|
108
|
-
add_to_serializer(:user, :wp_user_id) do
|
|
109
|
-
object.custom_fields['wp_user_id']
|
|
110
|
-
end
|
|
75
|
+
add_to_serializer(:user, :wp_user_id) { object.custom_fields['wp_user_id'] }
|
|
111
76
|
end
|
|
112
77
|
```
|
|
113
78
|
|
|
114
79
|
## Admin Routes + Controller
|
|
115
80
|
|
|
116
|
-
Every admin route needs `StaffConstraint` — never expose admin actions without it.
|
|
117
|
-
|
|
118
81
|
```ruby
|
|
119
|
-
#
|
|
82
|
+
# plugin.rb
|
|
120
83
|
add_admin_route 'my_plugin.title', 'my-plugin'
|
|
121
84
|
|
|
122
|
-
# Wire the route
|
|
123
85
|
Discourse::Application.routes.append do
|
|
124
|
-
get
|
|
125
|
-
|
|
126
|
-
post '/admin/plugins/my-plugin/action' => 'admin/my_plugin#action',
|
|
127
|
-
constraints: StaffConstraint.new
|
|
86
|
+
get '/admin/plugins/my-plugin' => 'admin/my_plugin#index', constraints: StaffConstraint.new
|
|
87
|
+
post '/admin/plugins/my-plugin/action' => 'admin/my_plugin#action', constraints: StaffConstraint.new
|
|
128
88
|
end
|
|
129
89
|
```
|
|
130
90
|
|
|
@@ -133,21 +93,15 @@ end
|
|
|
133
93
|
# frozen_string_literal: true
|
|
134
94
|
|
|
135
95
|
class ::Admin::MyPluginController < ::Admin::AdminController
|
|
136
|
-
#
|
|
137
|
-
# -
|
|
138
|
-
# - User is staff (moderator or admin)
|
|
139
|
-
# For admin-only actions, add:
|
|
96
|
+
# AdminController enforces: logged_in + staff
|
|
97
|
+
# For admin-only actions:
|
|
140
98
|
before_action :ensure_admin, only: [:dangerous_action]
|
|
141
99
|
|
|
142
100
|
def index
|
|
143
|
-
render json: {
|
|
144
|
-
stats: MyPlugin::Stats.summary,
|
|
145
|
-
settings: SiteSetting.my_plugin_enabled
|
|
146
|
-
}
|
|
101
|
+
render json: { stats: MyPlugin::Stats.summary, settings: SiteSetting.my_plugin_enabled }
|
|
147
102
|
end
|
|
148
103
|
|
|
149
104
|
def action
|
|
150
|
-
# Strong parameters for any mutating action
|
|
151
105
|
attrs = params.require(:my_plugin).permit(:field_one, :field_two)
|
|
152
106
|
result = MyPlugin::SomeService.call(attrs.to_h.symbolize_keys)
|
|
153
107
|
|
|
@@ -160,26 +114,18 @@ class ::Admin::MyPluginController < ::Admin::AdminController
|
|
|
160
114
|
end
|
|
161
115
|
```
|
|
162
116
|
|
|
163
|
-
## Guardian
|
|
164
|
-
|
|
165
|
-
Guardian is Discourse's authorization system. Always check it for user-facing actions.
|
|
117
|
+
## Guardian
|
|
166
118
|
|
|
167
119
|
```ruby
|
|
168
|
-
# Check permissions before acting
|
|
169
120
|
def update_post
|
|
170
121
|
post = Post.find(params[:id])
|
|
171
|
-
|
|
172
|
-
# guardian.can_edit_post? checks ownership, trust level, staff status
|
|
173
|
-
unless guardian.can_edit_post?(post)
|
|
174
|
-
return render json: failed_json, status: :forbidden
|
|
175
|
-
end
|
|
122
|
+
return render json: failed_json, status: :forbidden unless guardian.can_edit_post?(post)
|
|
176
123
|
|
|
177
124
|
post.update!(body: params[:body])
|
|
178
125
|
render json: PostSerializer.new(post, scope: guardian).as_json
|
|
179
126
|
end
|
|
180
127
|
|
|
181
|
-
#
|
|
182
|
-
# In after_initialize:
|
|
128
|
+
# Extend Guardian in after_initialize:
|
|
183
129
|
module ::Guardian::MyPluginExtensions
|
|
184
130
|
def can_use_my_feature?
|
|
185
131
|
authenticated? && (is_staff? || user.trust_level >= 2)
|
|
@@ -189,69 +135,42 @@ end
|
|
|
189
135
|
Guardian.prepend(::Guardian::MyPluginExtensions)
|
|
190
136
|
```
|
|
191
137
|
|
|
192
|
-
## Security
|
|
138
|
+
## Security
|
|
193
139
|
|
|
194
|
-
###
|
|
140
|
+
### SQL — no interpolation
|
|
195
141
|
|
|
196
142
|
```ruby
|
|
197
|
-
# BAD
|
|
143
|
+
# BAD
|
|
198
144
|
User.where("username = '#{params[:username]}'")
|
|
199
145
|
DB.query("SELECT * FROM users WHERE id = #{user_id}")
|
|
200
146
|
|
|
201
|
-
# GOOD
|
|
147
|
+
# GOOD
|
|
202
148
|
User.where(username: params[:username])
|
|
203
|
-
User.where("created_at > ?", params[:since])
|
|
204
|
-
|
|
205
|
-
# GOOD — Discourse DB helper with named bind params (always use this for raw SQL)
|
|
206
149
|
DB.query("SELECT * FROM users WHERE id = :id", id: user_id.to_i)
|
|
207
|
-
DB.query("UPDATE users SET flag = TRUE WHERE id = :id", id: user_id.to_i)
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
### 2. Staff-Only Admin Routes
|
|
211
|
-
|
|
212
|
-
```ruby
|
|
213
|
-
# ALWAYS use StaffConstraint on admin routes
|
|
214
|
-
get '/admin/plugins/my-plugin' => 'admin/my_plugin#index',
|
|
215
|
-
constraints: StaffConstraint.new
|
|
216
|
-
|
|
217
|
-
# Admin::AdminController gives you these checks automatically:
|
|
218
|
-
# - ensure_logged_in
|
|
219
|
-
# - ensure_staff
|
|
220
|
-
# For admin-only (not just moderator), add ensure_admin
|
|
221
150
|
```
|
|
222
151
|
|
|
223
|
-
###
|
|
152
|
+
### Logs — no sensitive values
|
|
224
153
|
|
|
225
154
|
```ruby
|
|
226
|
-
# BAD
|
|
227
|
-
Rails.logger.warn("[MyPlugin]
|
|
228
|
-
Rails.logger.info("[MyPlugin] Token: #{token}")
|
|
155
|
+
# BAD
|
|
156
|
+
Rails.logger.warn("[MyPlugin] Hash: #{user_hash}")
|
|
229
157
|
|
|
230
|
-
# GOOD
|
|
158
|
+
# GOOD
|
|
231
159
|
Rails.logger.info("[MyPlugin] Processing user #{user.id}")
|
|
232
|
-
Rails.logger.warn("[MyPlugin] Auth failed for user #{user.id}")
|
|
233
160
|
```
|
|
234
161
|
|
|
235
|
-
###
|
|
162
|
+
### Custom fields — register types
|
|
236
163
|
|
|
237
164
|
```ruby
|
|
238
|
-
# Register field types to prevent type confusion
|
|
239
165
|
register_user_custom_field_type('my_plugin_id', :integer)
|
|
240
|
-
register_post_custom_field_type('source_url', :string)
|
|
241
|
-
|
|
242
|
-
# Whitelist custom fields for API/serializer exposure
|
|
243
166
|
DiscoursePluginRegistry.serialized_current_user_fields << 'my_plugin_id'
|
|
244
|
-
|
|
245
|
-
# Access safely — always coerce type
|
|
246
167
|
user_id = (user.custom_fields['my_plugin_id'].to_i rescue nil)
|
|
247
168
|
```
|
|
248
169
|
|
|
249
|
-
###
|
|
170
|
+
### Rate limiting
|
|
250
171
|
|
|
251
172
|
```ruby
|
|
252
|
-
# For endpoints that test credentials or perform expensive operations
|
|
253
173
|
RateLimiter.new(current_user, "my_plugin_sensitive_action", 5, 1.minute).performed!
|
|
254
|
-
# Raises RateLimiter::LimitExceeded if over limit
|
|
255
174
|
```
|
|
256
175
|
|
|
257
176
|
## Site Settings
|
|
@@ -261,7 +180,7 @@ RateLimiter.new(current_user, "my_plugin_sensitive_action", 5, 1.minute).perform
|
|
|
261
180
|
plugins:
|
|
262
181
|
my_plugin_enabled:
|
|
263
182
|
default: false
|
|
264
|
-
client: false
|
|
183
|
+
client: false
|
|
265
184
|
my_plugin_max_items:
|
|
266
185
|
default: 100
|
|
267
186
|
min: 1
|
|
@@ -270,29 +189,22 @@ plugins:
|
|
|
270
189
|
```
|
|
271
190
|
|
|
272
191
|
```ruby
|
|
273
|
-
# Access in code
|
|
274
|
-
SiteSetting.my_plugin_enabled
|
|
275
|
-
SiteSetting.my_plugin_max_items
|
|
276
|
-
|
|
277
|
-
# Guard features
|
|
278
192
|
return unless SiteSetting.my_plugin_enabled
|
|
193
|
+
SiteSetting.my_plugin_max_items
|
|
279
194
|
```
|
|
280
195
|
|
|
281
196
|
## Migrations
|
|
282
197
|
|
|
283
198
|
```ruby
|
|
284
|
-
# db/migrate/20250101000000_create_my_plugin_records.rb
|
|
285
199
|
# frozen_string_literal: true
|
|
286
|
-
|
|
287
200
|
class CreateMyPluginRecords < ActiveRecord::Migration[7.0]
|
|
288
201
|
def change
|
|
289
202
|
create_table :my_plugin_records do |t|
|
|
290
203
|
t.integer :user_id, null: false
|
|
291
|
-
t.string
|
|
292
|
-
t.text
|
|
204
|
+
t.string :source_id, null: false
|
|
205
|
+
t.text :data
|
|
293
206
|
t.timestamps
|
|
294
207
|
end
|
|
295
|
-
|
|
296
208
|
add_index :my_plugin_records, :user_id
|
|
297
209
|
add_index :my_plugin_records, :source_id, unique: true
|
|
298
210
|
add_foreign_key :my_plugin_records, :users
|
|
@@ -300,20 +212,17 @@ class CreateMyPluginRecords < ActiveRecord::Migration[7.0]
|
|
|
300
212
|
end
|
|
301
213
|
```
|
|
302
214
|
|
|
303
|
-
## Import Script
|
|
304
|
-
|
|
305
|
-
Import scripts inherit from `ImportScripts::Base` and run outside the Rails request cycle.
|
|
215
|
+
## Import Script
|
|
306
216
|
|
|
307
217
|
```ruby
|
|
308
218
|
# frozen_string_literal: true
|
|
309
|
-
|
|
310
219
|
require_relative "base"
|
|
311
220
|
|
|
312
221
|
class ImportScripts::MyImport < ImportScripts::Base
|
|
313
222
|
def initialize
|
|
314
223
|
super
|
|
315
224
|
@client = Mysql2::Client.new(
|
|
316
|
-
host:
|
|
225
|
+
host: ENV.fetch('SOURCE_DB_HOST'),
|
|
317
226
|
username: ENV.fetch('SOURCE_DB_USER'),
|
|
318
227
|
password: ENV.fetch('SOURCE_DB_PASSWORD'),
|
|
319
228
|
database: ENV.fetch('SOURCE_DB_NAME')
|
|
@@ -329,26 +238,16 @@ class ImportScripts::MyImport < ImportScripts::Base
|
|
|
329
238
|
private
|
|
330
239
|
|
|
331
240
|
def import_users
|
|
332
|
-
puts "Importing users..."
|
|
333
|
-
|
|
334
|
-
# Parameterized query — never interpolate
|
|
335
241
|
users = @client.query(
|
|
336
242
|
"SELECT id, email, username, display_name FROM wp_users WHERE user_status = 0",
|
|
337
243
|
as: :hash
|
|
338
244
|
)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
{
|
|
342
|
-
id: user['id'],
|
|
343
|
-
email: user['email'],
|
|
344
|
-
username: normalize_username(user['username']),
|
|
345
|
-
name: user['display_name']
|
|
346
|
-
}
|
|
245
|
+
create_users(users) do |u|
|
|
246
|
+
{ id: u['id'], email: u['email'], username: normalize_username(u['username']), name: u['display_name'] }
|
|
347
247
|
end
|
|
348
248
|
end
|
|
349
249
|
|
|
350
250
|
def normalize_username(raw)
|
|
351
|
-
# Pure function — transform only, no side effects
|
|
352
251
|
raw.to_s.strip.downcase.gsub(/[^a-z0-9_]/, '_').truncate(20)
|
|
353
252
|
end
|
|
354
253
|
end
|
|
@@ -359,10 +258,8 @@ ImportScripts::MyImport.new.perform
|
|
|
359
258
|
## Testing
|
|
360
259
|
|
|
361
260
|
```ruby
|
|
362
|
-
# spec/plugin_helper.rb — loads Discourse test env
|
|
363
261
|
require 'rails_helper'
|
|
364
262
|
|
|
365
|
-
# spec/requests/admin/my_plugin_spec.rb
|
|
366
263
|
RSpec.describe Admin::MyPluginController do
|
|
367
264
|
fab!(:admin) { Fabricate(:admin) }
|
|
368
265
|
fab!(:user) { Fabricate(:user) }
|
|
@@ -370,27 +267,24 @@ RSpec.describe Admin::MyPluginController do
|
|
|
370
267
|
before { sign_in(admin) }
|
|
371
268
|
|
|
372
269
|
describe "GET #index" do
|
|
373
|
-
it "returns
|
|
270
|
+
it "returns 200 for admin" do
|
|
374
271
|
get "/admin/plugins/my-plugin.json"
|
|
375
272
|
expect(response.status).to eq(200)
|
|
376
273
|
end
|
|
377
274
|
end
|
|
378
275
|
|
|
379
276
|
describe "authorization" do
|
|
380
|
-
it "rejects non-staff" do
|
|
277
|
+
it "rejects non-staff with 404" do
|
|
381
278
|
sign_in(user)
|
|
382
279
|
get "/admin/plugins/my-plugin.json"
|
|
383
|
-
expect(response.status).to eq(404)
|
|
280
|
+
expect(response.status).to eq(404)
|
|
384
281
|
end
|
|
385
282
|
end
|
|
386
283
|
end
|
|
387
284
|
|
|
388
|
-
# Pure logic specs — no Discourse dependencies
|
|
389
285
|
RSpec.describe MyPlugin::UserSetup do
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
expect(described_class.normalize("John Doe!")).to eq("john_doe_")
|
|
393
|
-
end
|
|
286
|
+
it "normalizes username" do
|
|
287
|
+
expect(described_class.normalize("John Doe!")).to eq("john_doe_")
|
|
394
288
|
end
|
|
395
289
|
end
|
|
396
290
|
```
|
|
@@ -398,43 +292,34 @@ end
|
|
|
398
292
|
## Security Checklist
|
|
399
293
|
|
|
400
294
|
- [ ] `StaffConstraint.new` on all admin routes
|
|
401
|
-
- [ ]
|
|
402
|
-
- [ ] Strong
|
|
403
|
-
- [ ] No
|
|
404
|
-
- [ ] `guardian.can_*?`
|
|
405
|
-
- [ ] No sensitive values
|
|
406
|
-
- [ ] Rate limiting on credential-testing
|
|
407
|
-
- [ ] Custom field types registered
|
|
408
|
-
- [ ]
|
|
295
|
+
- [ ] Inherit `Admin::AdminController` for admin endpoints
|
|
296
|
+
- [ ] Strong params (`require().permit()`) on all mutations
|
|
297
|
+
- [ ] No SQL interpolation — use ActiveRecord or `DB.query` with `:named` params
|
|
298
|
+
- [ ] `guardian.can_*?` before user-facing mutations
|
|
299
|
+
- [ ] No sensitive values in logs
|
|
300
|
+
- [ ] Rate limiting on credential-testing / expensive endpoints
|
|
301
|
+
- [ ] Custom field types registered
|
|
302
|
+
- [ ] Feature flags via SiteSettings, not hardcoded booleans
|
|
409
303
|
|
|
410
304
|
## Common Pitfalls
|
|
411
305
|
|
|
412
|
-
| Pitfall |
|
|
413
|
-
|
|
414
|
-
| Monkey-patching Discourse classes |
|
|
415
|
-
|
|
|
416
|
-
|
|
|
417
|
-
| Logging `user.password_hash`
|
|
418
|
-
| `params[:field]`
|
|
419
|
-
| Hardcoded credentials
|
|
420
|
-
|
|
421
|
-
## When to Load Reference Files
|
|
422
|
-
|
|
423
|
-
### Security Examples
|
|
424
|
-
**File**: [`references/security.md`](references/security.md)
|
|
425
|
-
**Load when**: Implementing auth hooks, custom Guardian checks, SQL safety in import scripts
|
|
426
|
-
**Contains**: Guardian extension patterns, DB.query vs ActiveRecord, rate limiting, log hygiene
|
|
306
|
+
| Pitfall | Fix |
|
|
307
|
+
|---------|-----|
|
|
308
|
+
| Monkey-patching Discourse classes | `class_eval` / `prepend` in `after_initialize` |
|
|
309
|
+
| SQL string interpolation | `DB.query("… WHERE id = :id", id: val)` |
|
|
310
|
+
| `current_user.staff?` check in controller | Inherit `Admin::AdminController` |
|
|
311
|
+
| Logging `user.password_hash` | Log `user.id` only |
|
|
312
|
+
| Raw `params[:field]` | Always `params.require().permit()` |
|
|
313
|
+
| Hardcoded credentials | `ENV.fetch('KEY')` |
|
|
427
314
|
|
|
428
|
-
|
|
429
|
-
**File**: [`references/admin-ui.md`](references/admin-ui.md)
|
|
430
|
-
**Load when**: Building admin panel Ember components
|
|
431
|
-
**Contains**: Route setup, Ember component patterns, REST adapter, admin nav
|
|
315
|
+
## Reference Files
|
|
432
316
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
317
|
+
| File | Load when |
|
|
318
|
+
|------|-----------|
|
|
319
|
+
| [`references/security.md`](references/security.md) | Auth hooks, Guardian extensions, SQL safety in imports |
|
|
320
|
+
| [`references/admin-ui.md`](references/admin-ui.md) | Admin panel Ember components |
|
|
321
|
+
| [`references/import-scripts.md`](references/import-scripts.md) | Data migration: batching, lookup maps, idempotency |
|
|
437
322
|
|
|
438
323
|
---
|
|
439
324
|
|
|
440
|
-
**Evidence Base**: Discourse Developer Docs,
|
|
325
|
+
**Evidence Base**: Discourse Developer Docs, discourse-solved, discourse-data-explorer, Discourse CVE history (2024–2026), Rails Security Guide.
|