ima-claude 2.9.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 (182) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +463 -0
  3. package/dist/cli.js +1064 -0
  4. package/package.json +49 -0
  5. package/platforms/claude/adapter.ts +115 -0
  6. package/platforms/junie/adapter.ts +254 -0
  7. package/platforms/junie/agents-template.md +113 -0
  8. package/platforms/junie/hook-translations.md +84 -0
  9. package/platforms/shared/detector.ts +27 -0
  10. package/platforms/shared/installer.ts +202 -0
  11. package/platforms/shared/types.ts +78 -0
  12. package/plugins/ima-claude/.claude-plugin/plugin.json +25 -0
  13. package/plugins/ima-claude/agents/explorer.md +30 -0
  14. package/plugins/ima-claude/agents/implementer.md +30 -0
  15. package/plugins/ima-claude/agents/memory.md +42 -0
  16. package/plugins/ima-claude/agents/reviewer.md +53 -0
  17. package/plugins/ima-claude/agents/tester.md +33 -0
  18. package/plugins/ima-claude/agents/wp-developer.md +46 -0
  19. package/plugins/ima-claude/hooks/README.md +145 -0
  20. package/plugins/ima-claude/hooks/atlassian_prereqs.py +112 -0
  21. package/plugins/ima-claude/hooks/block_sed_edits.py +59 -0
  22. package/plugins/ima-claude/hooks/bootstrap.sh +90 -0
  23. package/plugins/ima-claude/hooks/bootstrap_utility_check.py +94 -0
  24. package/plugins/ima-claude/hooks/composer_autoload_check.py +70 -0
  25. package/plugins/ima-claude/hooks/docs_organization.py +104 -0
  26. package/plugins/ima-claude/hooks/enforce_rg_over_grep.py +56 -0
  27. package/plugins/ima-claude/hooks/fp_utility_check.py +90 -0
  28. package/plugins/ima-claude/hooks/hook_logger.py +69 -0
  29. package/plugins/ima-claude/hooks/hooks.json +239 -0
  30. package/plugins/ima-claude/hooks/jira_issue_fetch.py +79 -0
  31. package/plugins/ima-claude/hooks/jquery_in_wordpress.py +92 -0
  32. package/plugins/ima-claude/hooks/memory_bootstrap.py +79 -0
  33. package/plugins/ima-claude/hooks/memory_store_reminder.py +75 -0
  34. package/plugins/ima-claude/hooks/prompt_coach.py +125 -0
  35. package/plugins/ima-claude/hooks/prompt_coach_digest.md +48 -0
  36. package/plugins/ima-claude/hooks/prompt_coach_system.md +30 -0
  37. package/plugins/ima-claude/hooks/sequential_thinking_check.py +81 -0
  38. package/plugins/ima-claude/hooks/serena_over_grep.py +96 -0
  39. package/plugins/ima-claude/hooks/serena_over_read.py +66 -0
  40. package/plugins/ima-claude/hooks/serena_project_check.py +133 -0
  41. package/plugins/ima-claude/hooks/sql_injection_check.py +73 -0
  42. package/plugins/ima-claude/hooks/task_master_after_plan.py +31 -0
  43. package/plugins/ima-claude/hooks/task_master_before_impl.py +93 -0
  44. package/plugins/ima-claude/hooks/tavily_extract_advanced.py +48 -0
  45. package/plugins/ima-claude/hooks/vestige_before_external.py +86 -0
  46. package/plugins/ima-claude/hooks/webfetch_to_tavily.py +42 -0
  47. package/plugins/ima-claude/hooks/websearch_to_tavily.py +41 -0
  48. package/plugins/ima-claude/hooks/wp_security_check.py +150 -0
  49. package/plugins/ima-claude/personalities/README.md +45 -0
  50. package/plugins/ima-claude/personalities/enable-40k.md +69 -0
  51. package/plugins/ima-claude/personalities/enable-templars.md +69 -0
  52. package/plugins/ima-claude/skills/.research-summary.md +340 -0
  53. package/plugins/ima-claude/skills/architect/SKILL.md +304 -0
  54. package/plugins/ima-claude/skills/compound-bridge/SKILL.md +200 -0
  55. package/plugins/ima-claude/skills/discourse/SKILL.md +440 -0
  56. package/plugins/ima-claude/skills/discourse-admin/SKILL.md +192 -0
  57. package/plugins/ima-claude/skills/discourse-admin/references/api-endpoints.md +441 -0
  58. package/plugins/ima-claude/skills/discourse-admin/references/gotchas.md +107 -0
  59. package/plugins/ima-claude/skills/discourse-admin/references/staging-defaults.md +98 -0
  60. package/plugins/ima-claude/skills/discourse-admin/scripts/discourse-admin.py +319 -0
  61. package/plugins/ima-claude/skills/docs-organize/SKILL.md +254 -0
  62. package/plugins/ima-claude/skills/docs-organize/templates/active-README.md +50 -0
  63. package/plugins/ima-claude/skills/docs-organize/templates/archive-README.md +57 -0
  64. package/plugins/ima-claude/skills/docs-organize/templates/docs-README.md +43 -0
  65. package/plugins/ima-claude/skills/docs-organize/templates/phase-archive-README.md +83 -0
  66. package/plugins/ima-claude/skills/docs-organize/templates/section-README.md +48 -0
  67. package/plugins/ima-claude/skills/docs-organize/templates/transient-README.md +79 -0
  68. package/plugins/ima-claude/skills/docs-organize/templates/transient-gitignore +9 -0
  69. package/plugins/ima-claude/skills/ember-discourse/SKILL.md +496 -0
  70. package/plugins/ima-claude/skills/functional-programmer/SKILL.md +258 -0
  71. package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +278 -0
  72. package/plugins/ima-claude/skills/ima-bootstrap/references/bootstrap-patterns.md +356 -0
  73. package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +273 -0
  74. package/plugins/ima-claude/skills/ima-bootstrap/references/theme-integration.md +212 -0
  75. package/plugins/ima-claude/skills/ima-brand/SKILL.md +108 -0
  76. package/plugins/ima-claude/skills/ima-brand/references/brand-identity.md +140 -0
  77. package/plugins/ima-claude/skills/ima-brand/references/digital-standards.md +180 -0
  78. package/plugins/ima-claude/skills/ima-brand/references/visual-system.md +173 -0
  79. package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +175 -0
  80. package/plugins/ima-claude/skills/ima-forms-expert/references/container-components.md +154 -0
  81. package/plugins/ima-claude/skills/ima-forms-expert/references/examples.md +328 -0
  82. package/plugins/ima-claude/skills/ima-forms-expert/references/field-components.md +298 -0
  83. package/plugins/ima-claude/skills/ima-forms-expert/references/form-factory.md +193 -0
  84. package/plugins/ima-claude/skills/ima-forms-expert/references/quick-reference.md +153 -0
  85. package/plugins/ima-claude/skills/ima-forms-expert/references/validation-engine.md +336 -0
  86. package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +178 -0
  87. package/plugins/ima-claude/skills/jquery/SKILL.md +413 -0
  88. package/plugins/ima-claude/skills/js-fp/SKILL.md +463 -0
  89. package/plugins/ima-claude/skills/js-fp/core-principles.md +487 -0
  90. package/plugins/ima-claude/skills/js-fp/examples/pure-functions.js +260 -0
  91. package/plugins/ima-claude/skills/js-fp/examples/tests/pure-functions.test.js +262 -0
  92. package/plugins/ima-claude/skills/js-fp/references/anti-patterns.md +120 -0
  93. package/plugins/ima-claude/skills/js-fp/references/performance-patterns.md +116 -0
  94. package/plugins/ima-claude/skills/js-fp/references/testing-patterns.md +134 -0
  95. package/plugins/ima-claude/skills/js-fp-api/SKILL.md +280 -0
  96. package/plugins/ima-claude/skills/js-fp-api/examples/crud-endpoint.js +258 -0
  97. package/plugins/ima-claude/skills/js-fp-api/references/middleware-patterns.md +134 -0
  98. package/plugins/ima-claude/skills/js-fp-api/references/security-sql.md +110 -0
  99. package/plugins/ima-claude/skills/js-fp-api/references/validation-patterns.md +165 -0
  100. package/plugins/ima-claude/skills/js-fp-react/SKILL.md +447 -0
  101. package/plugins/ima-claude/skills/js-fp-react/examples/ProductCard.tsx +65 -0
  102. package/plugins/ima-claude/skills/js-fp-react/references/hooks-advanced.md +136 -0
  103. package/plugins/ima-claude/skills/js-fp-react/references/performance-patterns.md +175 -0
  104. package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +322 -0
  105. package/plugins/ima-claude/skills/js-fp-vue/references/complete-examples.md +397 -0
  106. package/plugins/ima-claude/skills/js-fp-vue/references/composables-advanced.md +282 -0
  107. package/plugins/ima-claude/skills/js-fp-vue/references/reactivity-patterns.md +348 -0
  108. package/plugins/ima-claude/skills/js-fp-vue/references/testing.md +314 -0
  109. package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +301 -0
  110. package/plugins/ima-claude/skills/js-fp-wordpress/references/ajax-patterns.md +192 -0
  111. package/plugins/ima-claude/skills/js-fp-wordpress/references/event-patterns.md +136 -0
  112. package/plugins/ima-claude/skills/js-fp-wordpress/references/wp-integration.md +248 -0
  113. package/plugins/ima-claude/skills/livecanvas/SKILL.md +209 -0
  114. package/plugins/ima-claude/skills/livecanvas/references/livecanvas-features.md +311 -0
  115. package/plugins/ima-claude/skills/livecanvas/references/loops-and-logic.md +730 -0
  116. package/plugins/ima-claude/skills/livecanvas/references/picostrap.md +227 -0
  117. package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +339 -0
  118. package/plugins/ima-claude/skills/mcp-context7/SKILL.md +109 -0
  119. package/plugins/ima-claude/skills/mcp-memory/SKILL.md +182 -0
  120. package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +233 -0
  121. package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +149 -0
  122. package/plugins/ima-claude/skills/mcp-serena/SKILL.md +174 -0
  123. package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +118 -0
  124. package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +259 -0
  125. package/plugins/ima-claude/skills/php-authnet/SKILL.md +275 -0
  126. package/plugins/ima-claude/skills/php-authnet/references/api-reference.md +624 -0
  127. package/plugins/ima-claude/skills/php-authnet/references/sandbox-testing.md +424 -0
  128. package/plugins/ima-claude/skills/php-fp/SKILL.md +333 -0
  129. package/plugins/ima-claude/skills/php-fp/examples/pure-functions.php +403 -0
  130. package/plugins/ima-claude/skills/php-fp/examples/tests/PureFunctionsTest.php +515 -0
  131. package/plugins/ima-claude/skills/php-fp/references/core-principles.md +277 -0
  132. package/plugins/ima-claude/skills/php-fp/references/testing-patterns.md +374 -0
  133. package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +216 -0
  134. package/plugins/ima-claude/skills/php-fp-wordpress/references/fp-patterns.md +275 -0
  135. package/plugins/ima-claude/skills/php-fp-wordpress/references/plugin-architecture.md +295 -0
  136. package/plugins/ima-claude/skills/php-fp-wordpress/references/security-examples.md +203 -0
  137. package/plugins/ima-claude/skills/php-fp-wordpress/references/testing-strategy.md +259 -0
  138. package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +716 -0
  139. package/plugins/ima-claude/skills/playwright/SKILL.md +434 -0
  140. package/plugins/ima-claude/skills/playwright/references/accessibility-testing.md +153 -0
  141. package/plugins/ima-claude/skills/playwright/references/ci-cd.md +268 -0
  142. package/plugins/ima-claude/skills/playwright/references/network-mocking.md +270 -0
  143. package/plugins/ima-claude/skills/playwright/references/visual-regression.md +215 -0
  144. package/plugins/ima-claude/skills/py-fp/SKILL.md +663 -0
  145. package/plugins/ima-claude/skills/py-fp/examples/pure-functions.py +185 -0
  146. package/plugins/ima-claude/skills/py-fp/examples/tests/test_pure_functions.py +244 -0
  147. package/plugins/ima-claude/skills/py-fp/references/core-principles.md +381 -0
  148. package/plugins/ima-claude/skills/py-fp/references/testing-patterns.md +283 -0
  149. package/plugins/ima-claude/skills/quasar-fp/SKILL.md +327 -0
  150. package/plugins/ima-claude/skills/quasar-fp/metadata.json +85 -0
  151. package/plugins/ima-claude/skills/quasar-fp/references/component-patterns.md +257 -0
  152. package/plugins/ima-claude/skills/quasar-fp/references/theme-integration.md +233 -0
  153. package/plugins/ima-claude/skills/quasar-fp/references/utility-classes.md +237 -0
  154. package/plugins/ima-claude/skills/quickstart/SKILL.md +129 -0
  155. package/plugins/ima-claude/skills/rails/SKILL.md +359 -0
  156. package/plugins/ima-claude/skills/resume-session/SKILL.md +68 -0
  157. package/plugins/ima-claude/skills/rg/SKILL.md +205 -0
  158. package/plugins/ima-claude/skills/ruby-fp/SKILL.md +336 -0
  159. package/plugins/ima-claude/skills/save-session/SKILL.md +81 -0
  160. package/plugins/ima-claude/skills/scorecard/SKILL.md +96 -0
  161. package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +127 -0
  162. package/plugins/ima-claude/skills/skill-analyzer/references/advanced-checklist.md +44 -0
  163. package/plugins/ima-claude/skills/skill-analyzer/references/core-checklist.md +60 -0
  164. package/plugins/ima-claude/skills/skill-analyzer/scripts/analyze_skill.py +418 -0
  165. package/plugins/ima-claude/skills/skill-creator/LICENSE.txt +202 -0
  166. package/plugins/ima-claude/skills/skill-creator/SKILL.md +343 -0
  167. package/plugins/ima-claude/skills/skill-creator/references/output-patterns.md +82 -0
  168. package/plugins/ima-claude/skills/skill-creator/references/workflows.md +28 -0
  169. package/plugins/ima-claude/skills/skill-creator/scripts/init_skill.py +303 -0
  170. package/plugins/ima-claude/skills/skill-creator/scripts/package_skill.py +110 -0
  171. package/plugins/ima-claude/skills/skill-creator/scripts/quick_validate.py +103 -0
  172. package/plugins/ima-claude/skills/task-master/SKILL.md +51 -0
  173. package/plugins/ima-claude/skills/task-planner/SKILL.md +228 -0
  174. package/plugins/ima-claude/skills/task-runner/SKILL.md +192 -0
  175. package/plugins/ima-claude/skills/unit-testing/SKILL.md +198 -0
  176. package/plugins/ima-claude/skills/unit-testing/references/mock-patterns.md +181 -0
  177. package/plugins/ima-claude/skills/unit-testing/references/tdd-workflow.md +177 -0
  178. package/plugins/ima-claude/skills/unit-testing/references/test-strategy.md +126 -0
  179. package/plugins/ima-claude/skills/wp-local/SKILL.md +246 -0
  180. package/plugins/ima-claude/skills/wp-local/references/configuration.md +198 -0
  181. package/plugins/ima-claude/skills/wp-local/references/wp-cli-reference.md +406 -0
  182. package/plugins/ima-claude/skills/wp-local/scripts/wp-local.sh +61 -0
@@ -0,0 +1,496 @@
1
+ ---
2
+ name: "ember-discourse"
3
+ description: "Ember/Glimmer component development for Discourse plugins - gjs, apiInitializer, plugin outlets, @tracked, @service, admin UI"
4
+ ---
5
+
6
+ # Ember for Discourse Plugins
7
+
8
+ Discourse runs Ember Octane with Glimmer components. The plugin extension model is: `apiInitializer` → `renderInOutlet` → `.gjs` component. Everything else is either legacy or an internal Discourse concern.
9
+
10
+ ## When to Use This Skill
11
+
12
+ - Adding frontend UI to a Discourse plugin
13
+ - Building admin panel components for a plugin
14
+ - Injecting content into Discourse's UI via plugin outlets
15
+ - Migrating old `decorateWidget` / raw handlebars to modern Glimmer
16
+
17
+ ## Core Philosophy
18
+
19
+ > Glimmer components are close to pure functions: `(args, services) → template`. Minimize `@tracked` state. Prefer derived values over stored state.
20
+
21
+ FP lens applied to Ember:
22
+ - **Components** are view functions — args in, DOM out
23
+ - **`@tracked`** is isolated reactive state — use sparingly, only what truly changes
24
+ - **Derived values** via getters — no duplicate state, always in sync
25
+ - **Actions** are the imperative shell — side effects live in methods, not templates
26
+ - **Services** are dependency injection — inject, don't import singletons
27
+
28
+ **Foundation**: Reference `../discourse/SKILL.md` for the Ruby side of plugin development.
29
+
30
+ ## The Modern Stack (use these)
31
+
32
+ | What | Modern | Deprecated / Avoid |
33
+ |------|--------|--------------------|
34
+ | File format | `.gjs` (Glimmer JS) | `.hbs` + `.js` pairs, `.hbr`, `.raw.hbs` |
35
+ | Entry point | `apiInitializer` | bare `withPluginApi` initializer export |
36
+ | Inject into UI | `api.renderInOutlet` | `registerConnectorClass`, `decorateWidget` |
37
+ | Component base | `@glimmer/component` | `@ember/component` (classic) |
38
+ | Reactive state | `@tracked` | `Ember.set()`, `this.set()` |
39
+ | Services | `@service` decorator | `Ember.inject.service()` |
40
+ | Event handling | `{{on "click" this.handler}}` | `{{action "handler"}}` |
41
+
42
+ ## .gjs: The File Format
43
+
44
+ `.gjs` (Glimmer JS) combines the component class and template in one file. It's the current Discourse standard for new components.
45
+
46
+ ```gjs
47
+ // assets/javascripts/discourse/components/my-component.gjs
48
+ import Component from "@glimmer/component";
49
+ import { tracked } from "@glimmer/tracking";
50
+ import { action } from "@ember/object";
51
+ import { service } from "@ember/service";
52
+ import DButton from "discourse/components/d-button";
53
+
54
+ export default class MyComponent extends Component {
55
+ @service currentUser;
56
+ @service siteSettings;
57
+
58
+ @tracked isExpanded = false;
59
+
60
+ // Derived value — getter, not stored state
61
+ get greeting() {
62
+ return this.currentUser
63
+ ? `Welcome back, ${this.currentUser.username}!`
64
+ : "Welcome to our community!";
65
+ }
66
+
67
+ @action
68
+ toggleExpanded() {
69
+ this.isExpanded = !this.isExpanded;
70
+ }
71
+
72
+ <template>
73
+ <div class="my-component">
74
+ <p>{{this.greeting}}</p>
75
+
76
+ <DButton
77
+ @action={{this.toggleExpanded}}
78
+ @label={{if this.isExpanded "collapse" "expand"}}
79
+ />
80
+
81
+ {{#if this.isExpanded}}
82
+ <div class="my-component__body">
83
+ {{yield}}
84
+ </div>
85
+ {{/if}}
86
+ </div>
87
+ </template>
88
+ }
89
+ ```
90
+
91
+ ### Template-only component (no JS needed)
92
+
93
+ ```gjs
94
+ // When a component is purely presentational — no class required
95
+ <template>
96
+ <div class="badge-card">
97
+ <img src={{@badge.image_url}} alt={{@badge.name}} />
98
+ <span>{{@badge.name}}</span>
99
+ </div>
100
+ </template>
101
+ ```
102
+
103
+ ## apiInitializer: The Plugin Entry Point
104
+
105
+ Every plugin's JS starts here. One initializer file per plugin feature area.
106
+
107
+ ```javascript
108
+ // assets/javascripts/discourse/initializers/my-plugin.js
109
+ import { apiInitializer } from "discourse/lib/api";
110
+ import MyBanner from "../components/my-banner";
111
+ import MyComponent from "../components/my-component";
112
+
113
+ export default apiInitializer((api) => {
114
+ // Render into a plugin outlet
115
+ api.renderInOutlet("discovery-list-container-top", MyBanner);
116
+
117
+ // shouldRender on the component class controls conditional rendering
118
+ api.renderInOutlet("topic-above-post-stream", MyComponent);
119
+ });
120
+ ```
121
+
122
+ ## Plugin Outlets: Where to Inject Content
123
+
124
+ Plugin outlets are pre-defined hooks in Discourse's templates. Use `api.renderInOutlet` to inject at them.
125
+
126
+ ```gjs
127
+ // assets/javascripts/discourse/components/my-banner.gjs
128
+ import Component from "@glimmer/component";
129
+ import { service } from "@ember/service";
130
+
131
+ export default class MyBanner extends Component {
132
+ @service currentUser;
133
+
134
+ // Static method controls whether this component renders at all.
135
+ // outletArgs contains context from where the outlet sits in the DOM.
136
+ static shouldRender(outletArgs, helper) {
137
+ return helper.siteSettings.my_plugin_enabled && helper.currentUser;
138
+ }
139
+
140
+ <template>
141
+ <div class="my-banner">
142
+ Welcome, {{this.currentUser.username}}!
143
+ </div>
144
+ </template>
145
+ }
146
+ ```
147
+
148
+ ### Post stream outlets (Glimmer post stream — current)
149
+
150
+ ```gjs
151
+ import Component from "@glimmer/component";
152
+ import { apiInitializer } from "discourse/lib/api";
153
+
154
+ export default apiInitializer((api) => {
155
+ api.renderAfterWrapperOutlet(
156
+ "post-content-cooked-html",
157
+ class extends Component {
158
+ static shouldRender(args) {
159
+ return args.post.wiki; // args.post is the current post model
160
+ }
161
+
162
+ <template>
163
+ <div class="wiki-notice">This post is a wiki</div>
164
+ </template>
165
+ }
166
+ );
167
+ });
168
+ ```
169
+
170
+ ### Finding outlet names
171
+
172
+ Search Discourse core for `<PluginOutlet @name=`:
173
+ ```bash
174
+ rg '<PluginOutlet @name=' app/assets/javascripts/discourse/
175
+ ```
176
+
177
+ Common outlets: `discovery-list-container-top`, `topic-above-post-stream`,
178
+ `above-main-container`, `header-icons`, `user-profile-primary`,
179
+ `post-content-cooked-html`, `after-topic-list-area`.
180
+
181
+ ## Glimmer Component Patterns
182
+
183
+ ### Args vs State
184
+
185
+ ```gjs
186
+ import Component from "@glimmer/component";
187
+ import { tracked } from "@glimmer/tracking";
188
+
189
+ export default class UserCard extends Component {
190
+ // @tracked — only for state THIS component owns and mutates
191
+ @tracked showDetails = false;
192
+
193
+ // Derived from args — a getter, never @tracked
194
+ get displayName() {
195
+ return this.args.user.name || this.args.user.username;
196
+ }
197
+
198
+ get isStaff() {
199
+ return this.args.user.staff;
200
+ }
201
+
202
+ <template>
203
+ <div class="user-card {{if this.isStaff 'user-card--staff'}}">
204
+ <h3>{{this.displayName}}</h3>
205
+ {{#if this.showDetails}}
206
+ <p>{{@user.bio_raw}}</p>
207
+ {{/if}}
208
+ </div>
209
+ </template>
210
+ }
211
+ ```
212
+
213
+ ### Services — inject, don't import
214
+
215
+ ```gjs
216
+ import Component from "@glimmer/component";
217
+ import { service } from "@ember/service";
218
+
219
+ export default class MyFeature extends Component {
220
+ // Common Discourse services
221
+ @service currentUser; // logged-in user (null if anonymous)
222
+ @service siteSettings; // site configuration
223
+ @service router; // programmatic navigation
224
+ @service store; // Ember Data store
225
+ @service session; // session data
226
+ @service modal; // open modals
227
+ @service toasts; // toast notifications (Discourse 3.2+)
228
+
229
+ get canUseFeature() {
230
+ return this.currentUser?.trust_level >= 2
231
+ && this.siteSettings.my_plugin_enabled;
232
+ }
233
+
234
+ <template>
235
+ {{#if this.canUseFeature}}
236
+ ...
237
+ {{/if}}
238
+ </template>
239
+ }
240
+ ```
241
+
242
+ ### Actions and events
243
+
244
+ ```gjs
245
+ import Component from "@glimmer/component";
246
+ import { tracked } from "@glimmer/tracking";
247
+ import { action } from "@ember/object";
248
+
249
+ export default class SearchBox extends Component {
250
+ @tracked query = "";
251
+ @tracked results = [];
252
+
253
+ @action
254
+ updateQuery(event) {
255
+ this.query = event.target.value;
256
+ }
257
+
258
+ @action
259
+ async search() {
260
+ if (!this.query.trim()) return;
261
+ // side effects belong in @action methods, not in getters or templates
262
+ this.results = await this.args.onSearch(this.query);
263
+ }
264
+
265
+ <template>
266
+ <input
267
+ type="text"
268
+ value={{this.query}}
269
+ {{on "input" this.updateQuery}}
270
+ />
271
+ <button type="button" {{on "click" this.search}}>Search</button>
272
+ </template>
273
+ }
274
+ ```
275
+
276
+ ## Admin UI Components
277
+
278
+ Admin components live in the `admin/` asset tree and use the same Glimmer patterns.
279
+
280
+ ```
281
+ assets/javascripts/
282
+ ├── discourse/
283
+ │ └── initializers/
284
+ │ └── my-plugin.js # user-facing outlets
285
+ └── admin/
286
+ ├── components/
287
+ │ └── my-plugin-admin.gjs # admin panel component
288
+ └── routes/
289
+ └── admin-plugins-my-plugin.js
290
+ ```
291
+
292
+ ```gjs
293
+ // assets/javascripts/admin/components/my-plugin-admin.gjs
294
+ import Component from "@glimmer/component";
295
+ import { tracked } from "@glimmer/tracking";
296
+ import { action } from "@ember/object";
297
+ import { service } from "@ember/service";
298
+ import { ajax } from "discourse/lib/ajax";
299
+ import { popupAjaxError } from "discourse/lib/ajax-error";
300
+ import DButton from "discourse/components/d-button";
301
+ import LoadingSpinner from "discourse/components/loading-spinner";
302
+
303
+ export default class MyPluginAdmin extends Component {
304
+ @service currentUser;
305
+
306
+ @tracked stats = null;
307
+ @tracked isLoading = false;
308
+
309
+ @action
310
+ async loadStats() {
311
+ this.isLoading = true;
312
+ try {
313
+ const response = await ajax("/admin/plugins/my-plugin.json");
314
+ this.stats = response.stats;
315
+ } catch (error) {
316
+ popupAjaxError(error);
317
+ } finally {
318
+ this.isLoading = false;
319
+ }
320
+ }
321
+
322
+ <template>
323
+ <div class="my-plugin-admin">
324
+ <h2>My Plugin Admin</h2>
325
+
326
+ {{#if this.isLoading}}
327
+ <LoadingSpinner />
328
+ {{else if this.stats}}
329
+ <p>Total users: {{this.stats.total_users}}</p>
330
+ <p>Pending: {{this.stats.pending}}</p>
331
+ {{/if}}
332
+
333
+ <DButton
334
+ @action={{this.loadStats}}
335
+ @label="my_plugin.admin.load_stats"
336
+ @disabled={{this.isLoading}}
337
+ />
338
+ </div>
339
+ </template>
340
+ }
341
+ ```
342
+
343
+ ## AJAX: Talking to the Plugin Backend
344
+
345
+ ```javascript
346
+ import { ajax } from "discourse/lib/ajax";
347
+ import { popupAjaxError } from "discourse/lib/ajax-error";
348
+
349
+ // GET
350
+ const data = await ajax("/my-plugin/endpoint.json");
351
+
352
+ // POST — Discourse's ajax helper automatically includes the CSRF token
353
+ const result = await ajax("/my-plugin/action", {
354
+ type: "POST",
355
+ data: { user_id: userId, value: someValue }
356
+ });
357
+
358
+ // Always handle errors with popupAjaxError for consistent UX
359
+ try {
360
+ await ajax("/my-plugin/action", { type: "DELETE" });
361
+ } catch (e) {
362
+ popupAjaxError(e);
363
+ }
364
+ ```
365
+
366
+ **Always use `discourse/lib/ajax`, never raw `fetch`.** The helper handles CSRF tokens, error formatting, and Discourse session state automatically.
367
+
368
+ ## Security in Ember
369
+
370
+ ### Client checks are UX only — backend enforces everything
371
+
372
+ ```javascript
373
+ // Hiding/showing UI based on role is fine:
374
+ get showAdminTools() {
375
+ return this.currentUser?.admin;
376
+ }
377
+
378
+ // But the BACKEND must enforce the same check on every request.
379
+ // Client-side auth is trivially bypassed in browser devtools.
380
+ // There is no such thing as client-side security.
381
+ ```
382
+
383
+ ### No raw HTML injection with user content
384
+
385
+ ```handlebars
386
+ {{!-- Safe — Glimmer auto-escapes output --}}
387
+ {{user.bio}}
388
+
389
+ {{!-- Unsafe — raw output, skip escaping only for server-sanitized HTML --}}
390
+ {{! avoid triple-mustache with user-supplied content }}
391
+
392
+ {{!-- For server-sanitized post content, Discourse provides: --}}
393
+ <div>{{html-safe post.cooked}}</div>
394
+ ```
395
+
396
+ ### Content Security Policy
397
+
398
+ Discourse enforces CSP strictly. These patterns will break or be blocked:
399
+ - Inline `<script>` tags in plugin templates
400
+ - Dynamic code evaluation (`eval`, dynamic code strings)
401
+ - Modifying `innerHTML` directly — use Glimmer templates instead
402
+ - Importing from external CDNs — bundle or use Discourse's asset pipeline
403
+
404
+ ## Deprecations to Actively Avoid
405
+
406
+ ```javascript
407
+ // DEPRECATED — old connector class pattern (shows deprecation warning)
408
+ api.registerConnectorClass("outlet-name", "connector-name", {
409
+ setupComponent(args, component) { ... }
410
+ });
411
+
412
+ // DEPRECATED — widget system (actively being removed in 2025/2026)
413
+ api.decorateWidget("post:after", (helper) => { ... });
414
+ api.createWidget("my-widget", { ... });
415
+
416
+ // DEPRECATED — raw handlebars files (.hbr, .raw.hbs)
417
+ // Breaks with the Glimmer topic list (enabled by default 2025)
418
+
419
+ // DEPRECATED — classic component base class
420
+ import Component from "@ember/component"; // use @glimmer/component instead
421
+
422
+ // DEPRECATED — Ember object mutation helpers
423
+ this.set("myProp", value); // use @tracked + direct assignment: this.myProp = value
424
+ Ember.set(obj, "key", val); // same — direct assignment or @tracked
425
+ ```
426
+
427
+ ## File Naming and Location
428
+
429
+ ```
430
+ assets/javascripts/discourse/
431
+ ├── initializers/
432
+ │ └── my-plugin.js # apiInitializer — one per feature area
433
+ ├── components/
434
+ │ ├── my-feature.gjs # kebab-case filenames
435
+ │ └── my-other-thing.gjs
436
+ └── lib/
437
+ └── my-utils.js # pure helpers, no Ember dependency
438
+
439
+ assets/javascripts/admin/
440
+ ├── components/
441
+ │ └── admin-my-plugin.gjs # admin components prefix with "admin-"
442
+ └── routes/
443
+ └── admin-plugins-my-plugin.js
444
+ ```
445
+
446
+ ## Practical Checklist
447
+
448
+ - [ ] Using `.gjs` (not `.hbs`/`.js` pairs) for new components
449
+ - [ ] Entry point is `apiInitializer`, not a bare `withPluginApi` export
450
+ - [ ] Plugin outlets via `api.renderInOutlet` — not `decorateWidget`
451
+ - [ ] `@tracked` only for state the component owns — not derived values
452
+ - [ ] Derived values are getters, not `@tracked` properties
453
+ - [ ] Services injected via `@service` — not imported as singletons
454
+ - [ ] AJAX via `discourse/lib/ajax` — not raw `fetch`
455
+ - [ ] Errors handled with `popupAjaxError`
456
+ - [ ] No raw HTML output with user-supplied content
457
+ - [ ] Backend enforces all authorization — client checks are UI-only
458
+
459
+ ## Quick Reference: Common Imports
460
+
461
+ ```javascript
462
+ // Glimmer/Ember core
463
+ import Component from "@glimmer/component";
464
+ import { tracked } from "@glimmer/tracking";
465
+ import { action } from "@ember/object";
466
+ import { service } from "@ember/service";
467
+
468
+ // Discourse plugin API
469
+ import { apiInitializer } from "discourse/lib/api";
470
+ import { ajax } from "discourse/lib/ajax";
471
+ import { popupAjaxError } from "discourse/lib/ajax-error";
472
+
473
+ // Discourse components
474
+ import DButton from "discourse/components/d-button";
475
+ import LoadingSpinner from "discourse/components/loading-spinner";
476
+ import DModal from "discourse/components/d-modal";
477
+
478
+ // i18n
479
+ import { i18n } from "discourse-i18n"; // current (replaces the old I18n.t())
480
+ ```
481
+
482
+ ## When to Load Reference Files
483
+
484
+ ### Admin UI Patterns
485
+ **File**: [`references/admin-ui.md`](references/admin-ui.md)
486
+ **Load when**: Building admin routes, tables, forms, settings UI
487
+ **Contains**: Full admin route + component example, admin table patterns, settings form
488
+
489
+ ### Plugin Outlet Reference
490
+ **File**: [`references/outlets.md`](references/outlets.md)
491
+ **Load when**: Need to find the right outlet or understand outletArgs context
492
+ **Contains**: Common outlet names, outletArgs by context, shouldRender patterns
493
+
494
+ ---
495
+
496
+ **Evidence Base**: Discourse Developer Docs (2025), Discourse Meta dev posts (Glimmer post stream migration Q1 2025, topic list migration), Ember Octane Guides, Discourse CVE history.