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,107 @@
1
+ # Discourse Admin API — Known Gotchas & Breaking Changes
2
+
3
+ ## Version History
4
+
5
+ Discourse moved from semantic versioning (3.x) to date-based versioning in January 2026.
6
+
7
+ | Old Version | New Version | Notes |
8
+ |-------------|-------------|-------|
9
+ | 3.5.0 | — | Last semantic version |
10
+ | — | 2026.1.0 | First ESR release, replaces "stable" |
11
+ | — | 2026.2.0 | Current as of March 2026 |
12
+
13
+ ## Breaking Changes
14
+
15
+ ### Custom User Fields URL Migration (3.5.0, late 2025)
16
+
17
+ **Old:** `/admin/customize/user_fields.json` — returns 404 on 3.5+
18
+ **New GET:** `/admin/config/user-fields.json` (hyphen) OR `/admin/config/user_fields.json` (underscore)
19
+ **New POST/PUT/DELETE:** `/admin/config/user_fields.json` (underscore only)
20
+
21
+ Both kebab-case and snake_case paths work for GET due to route aliases. For writes, use underscore.
22
+
23
+ ### Admin Route Migration Pattern
24
+
25
+ Discourse is progressively moving admin routes from `/admin/customize/` to `/admin/config/`. User fields were first. Expect other admin config endpoints to follow in future releases. Always verify against your Discourse version.
26
+
27
+ ### API Key Auth in Query Params (Deprecated)
28
+
29
+ Credentials MUST be passed as HTTP headers:
30
+ ```
31
+ Api-Key: <key>
32
+ Api-Username: <username>
33
+ ```
34
+
35
+ Query parameter auth (`?api_key=...&api_username=...`) is deprecated and may be removed.
36
+
37
+ ## Rate Limiting Gotchas
38
+
39
+ ### Two Independent Limiters
40
+
41
+ 1. **IP-based** — `RequestTracker` middleware, controlled by `max_reqs_per_ip_mode`
42
+ 2. **API key-based** — `DefaultCurrentUserProvider#admin_api_key_limiter`, uses `max_admin_api_reqs_per_minute` (default 60)
43
+
44
+ The API key limiter runs UNCONDITIONALLY regardless of `max_reqs_per_ip_mode`.
45
+
46
+ ### max=0 Means BLOCK ALL
47
+
48
+ Setting any rate limit to 0 means "block all requests", NOT "unlimited". Use high numbers (e.g., 6000) for effectively unlimited.
49
+
50
+ ### Redis Key Flush Required
51
+
52
+ After changing rate limit settings, you must flush the Redis rate limit keys. Settings changes alone won't clear existing rate limit counters.
53
+
54
+ ## Container / Server Gotchas
55
+
56
+ ### Pitchfork (Not Unicorn)
57
+
58
+ Dev containers use pitchfork, not unicorn. Do NOT send SIGHUP to pitchfork masters — it kills them instead of reloading. Restart the container instead.
59
+
60
+ ### No Versioned API
61
+
62
+ Discourse has NO versioned API (no `/api/v1/`). Endpoints can change between releases without formal deprecation. Always test against staging before upgrading Discourse.
63
+
64
+ ## Endpoint-Specific Gotchas
65
+
66
+ ### Silence User — Date Not Updated
67
+
68
+ `PUT /admin/users/{id}/silence.json` — if user is already silenced, the endpoint may NOT update the `silenced_till` date. Un-silence first, then re-silence.
69
+
70
+ ### Group Owner Removal — Different Param
71
+
72
+ `DELETE /admin/groups/{id}/owners.json` uses `user_id` (integer), NOT `usernames` (string). Every other member endpoint uses `usernames`.
73
+
74
+ ### Categories — Not Under /admin
75
+
76
+ Category CRUD is at `/categories.json`, NOT `/admin/categories.json`. Permission checks are inline via Guardian, not route constraints.
77
+
78
+ ### Hidden Settings
79
+
80
+ Some site settings are "hidden" and cannot be modified via the admin API (`SiteSetting.hidden_settings`). These include internal system settings that should not be changed via API.
81
+
82
+ ### Site Settings Update — Empty Response
83
+
84
+ `PUT /admin/site_settings/{name}.json` returns `200 OK` with an empty body on success. Don't expect the updated value in the response — re-fetch if you need to verify.
85
+
86
+ ### Bulk Update — Undocumented
87
+
88
+ `PUT /admin/site_settings/bulk_update.json` is not in the official API docs but is source-verified. It's the most efficient way to apply multiple settings changes.
89
+
90
+ ## Security Vulnerabilities (Patched)
91
+
92
+ ### CVE-2026-26265 (patched 2026.2.0)
93
+
94
+ IDOR in `/directory_items.json` allowed any user to retrieve private user field values via `user_field_ids` parameter. Patched to filter against `UserField.public_fields` for non-staff.
95
+
96
+ ### CVE-2026-21626 (patched 2026.1.0)
97
+
98
+ ACL bypass where custom fields JSON output lacked access control checks present on other serializers.
99
+
100
+ ## Self-Healing: Verifying Endpoints
101
+
102
+ When an endpoint returns unexpected results:
103
+
104
+ 1. **Check Discourse version**: `GET /admin/dashboard.json` → look for `version_check.installed_version`
105
+ 2. **Verify against source**: Check `config/routes.rb` in Discourse repo for current route definitions
106
+ 3. **Use Context7/Tavily**: Search for recent Discourse API changes
107
+ 4. **Check Meta**: https://meta.discourse.org for migration announcements
@@ -0,0 +1,98 @@
1
+ # Discourse Staging Environment — Recommended Defaults
2
+
3
+ Safe defaults for a staging Discourse instance. Apply these as an environment overlay on top of your base config.
4
+
5
+ ## Critical: Prevent External Communication
6
+
7
+ | Setting | Value | Why |
8
+ |---------|-------|-----|
9
+ | `disable_emails` | `yes` | Prevent ALL outbound email |
10
+ | `notification_email` | staging-specific address | Catch any that slip through |
11
+ | `contact_email` | staging-specific address | Don't expose real contact |
12
+ | `disable_digest_emails` | `true` | No digest emails to real users |
13
+
14
+ ## Visual Distinction
15
+
16
+ | Setting | Value | Why |
17
+ |---------|-------|-----|
18
+ | `title` | `[STAGING] {original title}` | Immediately obvious this is staging |
19
+ | `site_description` | `Staging environment — not for real use` | Clear in meta/search |
20
+ | `short_site_description` | `STAGING` | Short variant |
21
+ | `logo_url` | staging-branded logo if available | Visual cue |
22
+
23
+ ## SSO / DiscourseConnect
24
+
25
+ | Setting | Value | Why |
26
+ |---------|-------|-----|
27
+ | `discourse_connect_provider_secrets` | staging WP domains + secrets | Only staging WP sites can SSO |
28
+ | `enable_discourse_connect` | `true` (if using SSO) | Keep SSO active for testing |
29
+ | `discourse_connect_url` | staging SSO endpoint | Point to staging identity provider |
30
+
31
+ Format for `discourse_connect_provider_secrets`:
32
+ ```
33
+ staging.site1.com|secret1
34
+ staging.site2.com|secret2
35
+ ```
36
+
37
+ ## Access Control
38
+
39
+ | Setting | Value | Why |
40
+ |---------|-------|-----|
41
+ | `invite_only` | `true` | No public signups |
42
+ | `must_approve_users` | `true` | Extra safety gate |
43
+ | `enable_local_logins` | `true` | Allow direct login for testing |
44
+ | `min_password_length` | `6` | Easier test accounts |
45
+
46
+ ## Indexing Prevention
47
+
48
+ | Setting | Value | Why |
49
+ |---------|-------|-----|
50
+ | `allow_index_in_robots_txt` | `false` | Block search engines |
51
+ | `include_robots_info_header` | `false` | Don't advertise in headers |
52
+
53
+ ## Rate Limits (Relaxed for Testing)
54
+
55
+ | Setting | Value | Why |
56
+ |---------|-------|-----|
57
+ | `max_admin_api_reqs_per_minute` | `600` | Higher limit for automated testing |
58
+ | `max_reqs_per_ip_per_10_seconds` | `100` | Don't throttle test runners |
59
+
60
+ ## Webhook Safety
61
+
62
+ | Setting | Value | Why |
63
+ |---------|-------|-----|
64
+ | Webhooks | Point to staging receivers only | Never fire to production endpoints |
65
+
66
+ Review all configured webhooks at `/admin/api/web_hooks` and update URLs.
67
+
68
+ ## Sample Staging Overlay JSON
69
+
70
+ ```json
71
+ {
72
+ "title": "[STAGING] Community Forum",
73
+ "site_description": "Staging environment — not for real use",
74
+ "short_site_description": "STAGING",
75
+ "disable_emails": "yes",
76
+ "notification_email": "noreply-staging@example.com",
77
+ "contact_email": "staging-admin@example.com",
78
+ "disable_digest_emails": "true",
79
+ "invite_only": "true",
80
+ "must_approve_users": "true",
81
+ "enable_local_logins": "true",
82
+ "min_password_length": "6",
83
+ "allow_index_in_robots_txt": "false",
84
+ "max_admin_api_reqs_per_minute": "600",
85
+ "discourse_connect_provider_secrets": "staging.site1.com|secret\nstaging.site2.com|secret"
86
+ }
87
+ ```
88
+
89
+ ## Checklist: Before Deploying Config to Staging
90
+
91
+ - [ ] Email disabled (`disable_emails: yes`)
92
+ - [ ] Title prefixed with `[STAGING]`
93
+ - [ ] SSO secrets point to staging WP domains only
94
+ - [ ] Robots.txt blocks indexing
95
+ - [ ] Webhooks point to staging receivers
96
+ - [ ] Rate limits relaxed for test automation
97
+ - [ ] Public signup disabled (`invite_only: true`)
98
+ - [ ] Credentials are staging-specific (not production API keys)
@@ -0,0 +1,319 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Discourse Admin CLI — config-as-code for Discourse site settings.
4
+
5
+ Environment resolution (priority order):
6
+ 1. $DISCOURSE_ENV env var (e.g., "local", "staging", "production")
7
+ 2. .discourse-env file in project root or parents
8
+ 3. --env flag on command line
9
+ 4. Falls back to "local"
10
+
11
+ Credentials stored in: ~/.claude/discourse-environments.json
12
+
13
+ Usage:
14
+ discourse-admin.py export [output_file]
15
+ discourse-admin.py import <base_config> [overlay_config]
16
+ discourse-admin.py diff <config_file>
17
+ discourse-admin.py get <setting_name>
18
+ discourse-admin.py set <setting_name> <value>
19
+ discourse-admin.py envs
20
+ """
21
+
22
+ import json
23
+ import os
24
+ import sys
25
+ import urllib.request
26
+ import urllib.error
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+ ENVS_FILE = Path.home() / ".claude" / "discourse-environments.json"
31
+ ENV_FILE_NAME = ".discourse-env"
32
+ DEFAULT_ENV = "local"
33
+ DEFAULT_USERNAME = "system"
34
+
35
+
36
+ # ── Environment Resolution ──────────────────────────────────────────
37
+
38
+ def find_env_file() -> str | None:
39
+ """Walk up from cwd looking for .discourse-env file."""
40
+ d = Path.cwd()
41
+ while d != d.parent:
42
+ f = d / ENV_FILE_NAME
43
+ if f.is_file():
44
+ return f.read_text().strip()
45
+ d = d.parent
46
+ return None
47
+
48
+
49
+ def resolve_env(cli_env: str | None = None) -> str:
50
+ """Resolve environment name via priority chain."""
51
+ if cli_env:
52
+ return cli_env
53
+ if env_var := os.environ.get("DISCOURSE_ENV"):
54
+ return env_var
55
+ if file_env := find_env_file():
56
+ return file_env
57
+ return DEFAULT_ENV
58
+
59
+
60
+ def load_environments() -> dict:
61
+ """Load environment configs from ~/.claude/discourse-environments.json"""
62
+ if not ENVS_FILE.is_file():
63
+ print(f"Error: No environments configured at {ENVS_FILE}", file=sys.stderr)
64
+ print(f"Create it with:", file=sys.stderr)
65
+ print(json.dumps({
66
+ "local": {
67
+ "url": "http://localhost:4200",
68
+ "api_key": "your-api-key-here",
69
+ "api_username": "system"
70
+ },
71
+ "staging": {
72
+ "url": "https://staging.community.example.com",
73
+ "api_key": "staging-key",
74
+ "api_username": "system"
75
+ }
76
+ }, indent=2), file=sys.stderr)
77
+ sys.exit(1)
78
+ return json.loads(ENVS_FILE.read_text())
79
+
80
+
81
+ def get_credentials(env_name: str) -> dict:
82
+ """Get URL + credentials for a named environment."""
83
+ envs = load_environments()
84
+ if env_name not in envs:
85
+ available = ", ".join(envs.keys())
86
+ print(f"Error: Unknown environment '{env_name}'. Available: {available}", file=sys.stderr)
87
+ sys.exit(1)
88
+ cfg = envs[env_name]
89
+ return {
90
+ "url": cfg["url"].rstrip("/"),
91
+ "api_key": cfg["api_key"],
92
+ "api_username": cfg.get("api_username", DEFAULT_USERNAME),
93
+ }
94
+
95
+
96
+ # ── HTTP Client ─────────────────────────────────────────────────────
97
+
98
+ def api_request(creds: dict, method: str, endpoint: str, body: dict | None = None) -> Any:
99
+ """Make a Discourse API request. Returns parsed JSON or raises."""
100
+ url = f"{creds['url']}{endpoint}"
101
+ headers = {
102
+ "Api-Key": creds["api_key"],
103
+ "Api-Username": creds["api_username"],
104
+ "Content-Type": "application/json",
105
+ }
106
+
107
+ data = json.dumps(body).encode() if body else None
108
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
109
+
110
+ try:
111
+ with urllib.request.urlopen(req, timeout=30) as resp:
112
+ raw = resp.read().decode()
113
+ return json.loads(raw) if raw.strip() else {}
114
+ except urllib.error.HTTPError as e:
115
+ body_text = e.read().decode() if e.fp else ""
116
+ print(f"Error: HTTP {e.code} from {method} {endpoint}", file=sys.stderr)
117
+ if body_text:
118
+ try:
119
+ print(json.dumps(json.loads(body_text), indent=2), file=sys.stderr)
120
+ except json.JSONDecodeError:
121
+ print(body_text[:500], file=sys.stderr)
122
+ sys.exit(1)
123
+ except urllib.error.URLError as e:
124
+ print(f"Error: Cannot reach {url} — {e.reason}", file=sys.stderr)
125
+ sys.exit(1)
126
+
127
+
128
+ # ── Commands ────────────────────────────────────────────────────────
129
+
130
+ def cmd_export(creds: dict, output_file: str | None = None):
131
+ """Export non-default site settings to JSON."""
132
+ print(f"Fetching settings from {creds['url']}...", file=sys.stderr)
133
+ data = api_request(creds, "GET", "/admin/site_settings.json")
134
+
135
+ settings = data.get("site_settings", [])
136
+ non_default = {
137
+ s["setting"]: s["value"]
138
+ for s in settings
139
+ if s.get("value") != s.get("default") and s.get("setting")
140
+ }
141
+
142
+ result = json.dumps(non_default, indent=2, sort_keys=True)
143
+ print(f"Exported {len(non_default)} non-default settings.", file=sys.stderr)
144
+
145
+ if output_file:
146
+ Path(output_file).write_text(result + "\n")
147
+ print(f"Written to {output_file}", file=sys.stderr)
148
+ else:
149
+ print(result)
150
+
151
+
152
+ def cmd_import(creds: dict, base_file: str, overlay_file: str | None = None, dry_run: bool = False):
153
+ """Import settings from config file(s) with optional overlay merge."""
154
+ base = json.loads(Path(base_file).read_text())
155
+
156
+ if overlay_file:
157
+ overlay = json.loads(Path(overlay_file).read_text())
158
+ merged = {**base, **overlay}
159
+ print(f"Merged {base_file} ({len(base)} settings) + {overlay_file} ({len(overlay)} overrides) = {len(merged)} total", file=sys.stderr)
160
+ else:
161
+ merged = base
162
+ print(f"Loaded {len(merged)} settings from {base_file}", file=sys.stderr)
163
+
164
+ if dry_run:
165
+ print("\n=== DRY RUN — would apply: ===", file=sys.stderr)
166
+ for k, v in sorted(merged.items()):
167
+ val_display = str(v)[:80]
168
+ print(f" {k} = {val_display}", file=sys.stderr)
169
+ print(f"\nTotal: {len(merged)} settings. Run without --dry-run to apply.", file=sys.stderr)
170
+ return
171
+
172
+ # Try bulk_update first
173
+ payload = {
174
+ "settings": {k: {"value": str(v)} for k, v in merged.items()}
175
+ }
176
+
177
+ print(f"Applying {len(merged)} settings to {creds['url']}...", file=sys.stderr)
178
+
179
+ try:
180
+ api_request(creds, "PUT", "/admin/site_settings/bulk_update.json", payload)
181
+ print("Bulk update successful.", file=sys.stderr)
182
+ except SystemExit:
183
+ print("Bulk update failed. Falling back to individual updates...", file=sys.stderr)
184
+ success, fail = 0, 0
185
+ for name, value in sorted(merged.items()):
186
+ try:
187
+ api_request(creds, "PUT", f"/admin/site_settings/{name}.json", {name: str(value)})
188
+ print(f" OK: {name}", file=sys.stderr)
189
+ success += 1
190
+ except SystemExit:
191
+ print(f" FAIL: {name}", file=sys.stderr)
192
+ fail += 1
193
+ print(f"Done. {success} succeeded, {fail} failed.", file=sys.stderr)
194
+
195
+
196
+ def cmd_diff(creds: dict, config_file: str):
197
+ """Show what would change if config were applied (compare remote vs local file)."""
198
+ desired = json.loads(Path(config_file).read_text())
199
+
200
+ print(f"Fetching current settings from {creds['url']}...", file=sys.stderr)
201
+ data = api_request(creds, "GET", "/admin/site_settings.json")
202
+
203
+ current = {s["setting"]: s["value"] for s in data.get("site_settings", []) if s.get("setting")}
204
+
205
+ changes, no_change, missing = [], [], []
206
+ for name, desired_val in sorted(desired.items()):
207
+ if name not in current:
208
+ missing.append(name)
209
+ elif str(current[name]) != str(desired_val):
210
+ changes.append((name, current[name], desired_val))
211
+ else:
212
+ no_change.append(name)
213
+
214
+ if changes:
215
+ print(f"\n{'Setting':<45} {'Current':<30} {'Desired':<30}")
216
+ print("-" * 105)
217
+ for name, cur, des in changes:
218
+ print(f" {name:<43} {str(cur)[:28]:<30} {str(des)[:28]:<30}")
219
+
220
+ print(f"\nSummary: {len(changes)} changes, {len(no_change)} unchanged, {len(missing)} unknown settings")
221
+
222
+
223
+ def cmd_get(creds: dict, setting_name: str):
224
+ """Get a single setting's current value."""
225
+ data = api_request(creds, "GET", "/admin/site_settings.json")
226
+ for s in data.get("site_settings", []):
227
+ if s.get("setting") == setting_name:
228
+ print(json.dumps({
229
+ "setting": s["setting"],
230
+ "value": s["value"],
231
+ "default": s.get("default"),
232
+ "type": s.get("type"),
233
+ "description": s.get("description", ""),
234
+ }, indent=2))
235
+ return
236
+ print(f"Setting '{setting_name}' not found.", file=sys.stderr)
237
+ sys.exit(1)
238
+
239
+
240
+ def cmd_set(creds: dict, setting_name: str, value: str):
241
+ """Set a single setting."""
242
+ api_request(creds, "PUT", f"/admin/site_settings/{setting_name}.json", {setting_name: value})
243
+ print(f"OK: {setting_name} = {value}", file=sys.stderr)
244
+
245
+
246
+ def cmd_envs():
247
+ """List configured environments."""
248
+ if not ENVS_FILE.is_file():
249
+ print(f"No environments configured. Create {ENVS_FILE}", file=sys.stderr)
250
+ sys.exit(1)
251
+ envs = json.loads(ENVS_FILE.read_text())
252
+ current = resolve_env()
253
+ for name, cfg in envs.items():
254
+ marker = " ← active" if name == current else ""
255
+ print(f" {name}: {cfg['url']}{marker}")
256
+
257
+
258
+ # ── CLI Entry Point ─────────────────────────────────────────────────
259
+
260
+ def main():
261
+ args = sys.argv[1:]
262
+
263
+ if not args or args[0] in ("-h", "--help", "help"):
264
+ print(__doc__)
265
+ sys.exit(0)
266
+
267
+ # Parse --env flag
268
+ env_override = None
269
+ if "--env" in args:
270
+ idx = args.index("--env")
271
+ if idx + 1 < len(args):
272
+ env_override = args[idx + 1]
273
+ args = args[:idx] + args[idx + 2:]
274
+
275
+ dry_run = "--dry-run" in args
276
+ if dry_run:
277
+ args.remove("--dry-run")
278
+
279
+ cmd = args[0]
280
+ cmd_args = args[1:]
281
+
282
+ if cmd == "envs":
283
+ cmd_envs()
284
+ return
285
+
286
+ env_name = resolve_env(env_override)
287
+ creds = get_credentials(env_name)
288
+ print(f"[{env_name}] {creds['url']}", file=sys.stderr)
289
+
290
+ if cmd == "export":
291
+ cmd_export(creds, cmd_args[0] if cmd_args else None)
292
+ elif cmd == "import":
293
+ if not cmd_args:
294
+ print("Usage: discourse-admin.py import <base_config> [overlay_config]", file=sys.stderr)
295
+ sys.exit(1)
296
+ cmd_import(creds, cmd_args[0], cmd_args[1] if len(cmd_args) > 1 else None, dry_run=dry_run)
297
+ elif cmd == "diff":
298
+ if not cmd_args:
299
+ print("Usage: discourse-admin.py diff <config_file>", file=sys.stderr)
300
+ sys.exit(1)
301
+ cmd_diff(creds, cmd_args[0])
302
+ elif cmd == "get":
303
+ if not cmd_args:
304
+ print("Usage: discourse-admin.py get <setting_name>", file=sys.stderr)
305
+ sys.exit(1)
306
+ cmd_get(creds, cmd_args[0])
307
+ elif cmd == "set":
308
+ if len(cmd_args) < 2:
309
+ print("Usage: discourse-admin.py set <setting_name> <value>", file=sys.stderr)
310
+ sys.exit(1)
311
+ cmd_set(creds, cmd_args[0], cmd_args[1])
312
+ else:
313
+ print(f"Unknown command: {cmd}", file=sys.stderr)
314
+ print("Commands: export, import, diff, get, set, envs", file=sys.stderr)
315
+ sys.exit(1)
316
+
317
+
318
+ if __name__ == "__main__":
319
+ main()