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.
- package/LICENSE +21 -0
- package/README.md +463 -0
- package/dist/cli.js +1064 -0
- package/package.json +49 -0
- package/platforms/claude/adapter.ts +115 -0
- package/platforms/junie/adapter.ts +254 -0
- package/platforms/junie/agents-template.md +113 -0
- package/platforms/junie/hook-translations.md +84 -0
- package/platforms/shared/detector.ts +27 -0
- package/platforms/shared/installer.ts +202 -0
- package/platforms/shared/types.ts +78 -0
- package/plugins/ima-claude/.claude-plugin/plugin.json +25 -0
- package/plugins/ima-claude/agents/explorer.md +30 -0
- package/plugins/ima-claude/agents/implementer.md +30 -0
- package/plugins/ima-claude/agents/memory.md +42 -0
- package/plugins/ima-claude/agents/reviewer.md +53 -0
- package/plugins/ima-claude/agents/tester.md +33 -0
- package/plugins/ima-claude/agents/wp-developer.md +46 -0
- package/plugins/ima-claude/hooks/README.md +145 -0
- package/plugins/ima-claude/hooks/atlassian_prereqs.py +112 -0
- package/plugins/ima-claude/hooks/block_sed_edits.py +59 -0
- package/plugins/ima-claude/hooks/bootstrap.sh +90 -0
- package/plugins/ima-claude/hooks/bootstrap_utility_check.py +94 -0
- package/plugins/ima-claude/hooks/composer_autoload_check.py +70 -0
- package/plugins/ima-claude/hooks/docs_organization.py +104 -0
- package/plugins/ima-claude/hooks/enforce_rg_over_grep.py +56 -0
- package/plugins/ima-claude/hooks/fp_utility_check.py +90 -0
- package/plugins/ima-claude/hooks/hook_logger.py +69 -0
- package/plugins/ima-claude/hooks/hooks.json +239 -0
- package/plugins/ima-claude/hooks/jira_issue_fetch.py +79 -0
- package/plugins/ima-claude/hooks/jquery_in_wordpress.py +92 -0
- package/plugins/ima-claude/hooks/memory_bootstrap.py +79 -0
- package/plugins/ima-claude/hooks/memory_store_reminder.py +75 -0
- package/plugins/ima-claude/hooks/prompt_coach.py +125 -0
- package/plugins/ima-claude/hooks/prompt_coach_digest.md +48 -0
- package/plugins/ima-claude/hooks/prompt_coach_system.md +30 -0
- package/plugins/ima-claude/hooks/sequential_thinking_check.py +81 -0
- package/plugins/ima-claude/hooks/serena_over_grep.py +96 -0
- package/plugins/ima-claude/hooks/serena_over_read.py +66 -0
- package/plugins/ima-claude/hooks/serena_project_check.py +133 -0
- package/plugins/ima-claude/hooks/sql_injection_check.py +73 -0
- package/plugins/ima-claude/hooks/task_master_after_plan.py +31 -0
- package/plugins/ima-claude/hooks/task_master_before_impl.py +93 -0
- package/plugins/ima-claude/hooks/tavily_extract_advanced.py +48 -0
- package/plugins/ima-claude/hooks/vestige_before_external.py +86 -0
- package/plugins/ima-claude/hooks/webfetch_to_tavily.py +42 -0
- package/plugins/ima-claude/hooks/websearch_to_tavily.py +41 -0
- package/plugins/ima-claude/hooks/wp_security_check.py +150 -0
- package/plugins/ima-claude/personalities/README.md +45 -0
- package/plugins/ima-claude/personalities/enable-40k.md +69 -0
- package/plugins/ima-claude/personalities/enable-templars.md +69 -0
- package/plugins/ima-claude/skills/.research-summary.md +340 -0
- package/plugins/ima-claude/skills/architect/SKILL.md +304 -0
- package/plugins/ima-claude/skills/compound-bridge/SKILL.md +200 -0
- package/plugins/ima-claude/skills/discourse/SKILL.md +440 -0
- package/plugins/ima-claude/skills/discourse-admin/SKILL.md +192 -0
- package/plugins/ima-claude/skills/discourse-admin/references/api-endpoints.md +441 -0
- package/plugins/ima-claude/skills/discourse-admin/references/gotchas.md +107 -0
- package/plugins/ima-claude/skills/discourse-admin/references/staging-defaults.md +98 -0
- package/plugins/ima-claude/skills/discourse-admin/scripts/discourse-admin.py +319 -0
- package/plugins/ima-claude/skills/docs-organize/SKILL.md +254 -0
- package/plugins/ima-claude/skills/docs-organize/templates/active-README.md +50 -0
- package/plugins/ima-claude/skills/docs-organize/templates/archive-README.md +57 -0
- package/plugins/ima-claude/skills/docs-organize/templates/docs-README.md +43 -0
- package/plugins/ima-claude/skills/docs-organize/templates/phase-archive-README.md +83 -0
- package/plugins/ima-claude/skills/docs-organize/templates/section-README.md +48 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-README.md +79 -0
- package/plugins/ima-claude/skills/docs-organize/templates/transient-gitignore +9 -0
- package/plugins/ima-claude/skills/ember-discourse/SKILL.md +496 -0
- package/plugins/ima-claude/skills/functional-programmer/SKILL.md +258 -0
- package/plugins/ima-claude/skills/ima-bootstrap/SKILL.md +278 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/bootstrap-patterns.md +356 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/ima-brand.md +273 -0
- package/plugins/ima-claude/skills/ima-bootstrap/references/theme-integration.md +212 -0
- package/plugins/ima-claude/skills/ima-brand/SKILL.md +108 -0
- package/plugins/ima-claude/skills/ima-brand/references/brand-identity.md +140 -0
- package/plugins/ima-claude/skills/ima-brand/references/digital-standards.md +180 -0
- package/plugins/ima-claude/skills/ima-brand/references/visual-system.md +173 -0
- package/plugins/ima-claude/skills/ima-forms-expert/SKILL.md +175 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/container-components.md +154 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/examples.md +328 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/field-components.md +298 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/form-factory.md +193 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/quick-reference.md +153 -0
- package/plugins/ima-claude/skills/ima-forms-expert/references/validation-engine.md +336 -0
- package/plugins/ima-claude/skills/jira-checkpoint/SKILL.md +178 -0
- package/plugins/ima-claude/skills/jquery/SKILL.md +413 -0
- package/plugins/ima-claude/skills/js-fp/SKILL.md +463 -0
- package/plugins/ima-claude/skills/js-fp/core-principles.md +487 -0
- package/plugins/ima-claude/skills/js-fp/examples/pure-functions.js +260 -0
- package/plugins/ima-claude/skills/js-fp/examples/tests/pure-functions.test.js +262 -0
- package/plugins/ima-claude/skills/js-fp/references/anti-patterns.md +120 -0
- package/plugins/ima-claude/skills/js-fp/references/performance-patterns.md +116 -0
- package/plugins/ima-claude/skills/js-fp/references/testing-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/SKILL.md +280 -0
- package/plugins/ima-claude/skills/js-fp-api/examples/crud-endpoint.js +258 -0
- package/plugins/ima-claude/skills/js-fp-api/references/middleware-patterns.md +134 -0
- package/plugins/ima-claude/skills/js-fp-api/references/security-sql.md +110 -0
- package/plugins/ima-claude/skills/js-fp-api/references/validation-patterns.md +165 -0
- package/plugins/ima-claude/skills/js-fp-react/SKILL.md +447 -0
- package/plugins/ima-claude/skills/js-fp-react/examples/ProductCard.tsx +65 -0
- package/plugins/ima-claude/skills/js-fp-react/references/hooks-advanced.md +136 -0
- package/plugins/ima-claude/skills/js-fp-react/references/performance-patterns.md +175 -0
- package/plugins/ima-claude/skills/js-fp-vue/SKILL.md +322 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/complete-examples.md +397 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/composables-advanced.md +282 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/reactivity-patterns.md +348 -0
- package/plugins/ima-claude/skills/js-fp-vue/references/testing.md +314 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/SKILL.md +301 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/ajax-patterns.md +192 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/event-patterns.md +136 -0
- package/plugins/ima-claude/skills/js-fp-wordpress/references/wp-integration.md +248 -0
- package/plugins/ima-claude/skills/livecanvas/SKILL.md +209 -0
- package/plugins/ima-claude/skills/livecanvas/references/livecanvas-features.md +311 -0
- package/plugins/ima-claude/skills/livecanvas/references/loops-and-logic.md +730 -0
- package/plugins/ima-claude/skills/livecanvas/references/picostrap.md +227 -0
- package/plugins/ima-claude/skills/mcp-atlassian/SKILL.md +339 -0
- package/plugins/ima-claude/skills/mcp-context7/SKILL.md +109 -0
- package/plugins/ima-claude/skills/mcp-memory/SKILL.md +182 -0
- package/plugins/ima-claude/skills/mcp-qdrant/SKILL.md +233 -0
- package/plugins/ima-claude/skills/mcp-sequential/SKILL.md +149 -0
- package/plugins/ima-claude/skills/mcp-serena/SKILL.md +174 -0
- package/plugins/ima-claude/skills/mcp-tavily/SKILL.md +118 -0
- package/plugins/ima-claude/skills/mcp-vestige/SKILL.md +259 -0
- package/plugins/ima-claude/skills/php-authnet/SKILL.md +275 -0
- package/plugins/ima-claude/skills/php-authnet/references/api-reference.md +624 -0
- package/plugins/ima-claude/skills/php-authnet/references/sandbox-testing.md +424 -0
- package/plugins/ima-claude/skills/php-fp/SKILL.md +333 -0
- package/plugins/ima-claude/skills/php-fp/examples/pure-functions.php +403 -0
- package/plugins/ima-claude/skills/php-fp/examples/tests/PureFunctionsTest.php +515 -0
- package/plugins/ima-claude/skills/php-fp/references/core-principles.md +277 -0
- package/plugins/ima-claude/skills/php-fp/references/testing-patterns.md +374 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/SKILL.md +216 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/fp-patterns.md +275 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/plugin-architecture.md +295 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/security-examples.md +203 -0
- package/plugins/ima-claude/skills/php-fp-wordpress/references/testing-strategy.md +259 -0
- package/plugins/ima-claude/skills/phpunit-wp/SKILL.md +716 -0
- package/plugins/ima-claude/skills/playwright/SKILL.md +434 -0
- package/plugins/ima-claude/skills/playwright/references/accessibility-testing.md +153 -0
- package/plugins/ima-claude/skills/playwright/references/ci-cd.md +268 -0
- package/plugins/ima-claude/skills/playwright/references/network-mocking.md +270 -0
- package/plugins/ima-claude/skills/playwright/references/visual-regression.md +215 -0
- package/plugins/ima-claude/skills/py-fp/SKILL.md +663 -0
- package/plugins/ima-claude/skills/py-fp/examples/pure-functions.py +185 -0
- package/plugins/ima-claude/skills/py-fp/examples/tests/test_pure_functions.py +244 -0
- package/plugins/ima-claude/skills/py-fp/references/core-principles.md +381 -0
- package/plugins/ima-claude/skills/py-fp/references/testing-patterns.md +283 -0
- package/plugins/ima-claude/skills/quasar-fp/SKILL.md +327 -0
- package/plugins/ima-claude/skills/quasar-fp/metadata.json +85 -0
- package/plugins/ima-claude/skills/quasar-fp/references/component-patterns.md +257 -0
- package/plugins/ima-claude/skills/quasar-fp/references/theme-integration.md +233 -0
- package/plugins/ima-claude/skills/quasar-fp/references/utility-classes.md +237 -0
- package/plugins/ima-claude/skills/quickstart/SKILL.md +129 -0
- package/plugins/ima-claude/skills/rails/SKILL.md +359 -0
- package/plugins/ima-claude/skills/resume-session/SKILL.md +68 -0
- package/plugins/ima-claude/skills/rg/SKILL.md +205 -0
- package/plugins/ima-claude/skills/ruby-fp/SKILL.md +336 -0
- package/plugins/ima-claude/skills/save-session/SKILL.md +81 -0
- package/plugins/ima-claude/skills/scorecard/SKILL.md +96 -0
- package/plugins/ima-claude/skills/skill-analyzer/SKILL.md +127 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/advanced-checklist.md +44 -0
- package/plugins/ima-claude/skills/skill-analyzer/references/core-checklist.md +60 -0
- package/plugins/ima-claude/skills/skill-analyzer/scripts/analyze_skill.py +418 -0
- package/plugins/ima-claude/skills/skill-creator/LICENSE.txt +202 -0
- package/plugins/ima-claude/skills/skill-creator/SKILL.md +343 -0
- package/plugins/ima-claude/skills/skill-creator/references/output-patterns.md +82 -0
- package/plugins/ima-claude/skills/skill-creator/references/workflows.md +28 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/init_skill.py +303 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/package_skill.py +110 -0
- package/plugins/ima-claude/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/plugins/ima-claude/skills/task-master/SKILL.md +51 -0
- package/plugins/ima-claude/skills/task-planner/SKILL.md +228 -0
- package/plugins/ima-claude/skills/task-runner/SKILL.md +192 -0
- package/plugins/ima-claude/skills/unit-testing/SKILL.md +198 -0
- package/plugins/ima-claude/skills/unit-testing/references/mock-patterns.md +181 -0
- package/plugins/ima-claude/skills/unit-testing/references/tdd-workflow.md +177 -0
- package/plugins/ima-claude/skills/unit-testing/references/test-strategy.md +126 -0
- package/plugins/ima-claude/skills/wp-local/SKILL.md +246 -0
- package/plugins/ima-claude/skills/wp-local/references/configuration.md +198 -0
- package/plugins/ima-claude/skills/wp-local/references/wp-cli-reference.md +406 -0
- 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()
|