Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
5 changes: 3 additions & 2 deletions .agents/skills/add-admin-api-endpoint/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ description: Add a new endpoint or endpoints to Ghost's Admin API at `ghost/api/
## Instructions

1. If creating an endpoint for an entirely new resource, create a new endpoint file in `ghost/core/core/server/api/endpoints/`. Otherwise, locate the existing endpoint file in the same directory.
2. The endpoint file should create a controller object using the JSDoc type from (@tryghost/api-framework).Controller, including at minimum a `docName` and a single endpoint definition, i.e. `browse`.
2. The endpoint file should create a controller object using the JSDoc type from (@tryghost/api-framework).Controller, including at minimum a `docName` and a single endpoint definition, i.e. `browse`.
3. Add routes for each endpoint to `ghost/core/core/server/web/api/endpoints/admin/routes.js`.
4. Add basic `e2e-api` tests for the endpoint in `ghost/core/test/e2e-api/admin` to ensure the new endpoints function as expected.
5. Run the tests and iterate until they pass: `cd ghost/core && pnpm test:single test/e2e-api/admin/{test-file-name}`.

## Reference
For a detailed reference on Ghost's API framework and how to create API controllers, see [reference.md](reference.md).

For a detailed reference on Ghost's API framework and how to create API controllers, see [reference.md](reference.md).
23 changes: 16 additions & 7 deletions .agents/skills/add-admin-api-endpoint/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ edit: {
```

**When to use:**

- Standard CRUD operations
- When the default permission handler meets your needs
- Most common case for authenticated endpoints
Expand All @@ -83,11 +84,13 @@ When you set `permissions: true`, the framework delegates to the default permiss
- `categories` → `category` (handles `ies` → `y`)

2. **Permission Check**: It calls the permissions service:

```javascript
permissions.canThis(frame.options.context)[method][singular](identifier, unsafeAttrs)
```

For example, with `docName: 'posts'` and method `edit`:

```javascript
permissions.canThis(context).edit.post(postId, unsafeAttrs)
```
Expand All @@ -102,6 +105,7 @@ When you set `permissions: true`, the framework delegates to the default permiss
For the default handler to work, you must have:

1. **Permission records** in the `permissions` table:

```sql
INSERT INTO permissions (name, action_type, object_type) VALUES
('Browse posts', 'browse', 'post'),
Expand All @@ -114,6 +118,7 @@ For the default handler to work, you must have:
2. **Role-permission mappings** in `permissions_roles` linking permissions to roles like Administrator, Editor, etc.

These are typically added via:

- Initial fixtures in `ghost/core/core/server/data/schema/fixtures/fixtures.json`
- Database migrations using `addPermissionWithRoles()` from `ghost/core/core/server/data/migrations/utils/permissions.js`

Expand All @@ -134,6 +139,7 @@ browse: {
```

**When to use:**

- Public endpoints that don't require authentication
- Health check or status endpoints
- Resources that should be accessible to everyone
Expand Down Expand Up @@ -177,6 +183,7 @@ delete: {
```

**When to use:**

- Complex permission logic that varies by resource
- Owner-based permissions
- Role-based access control beyond the default handler
Expand Down Expand Up @@ -205,6 +212,7 @@ edit: {
```

**When to use:**

- Default permission handler is sufficient but needs configuration
- You have attributes that require special permission handling
- You need to prepare data before permission checks run
Expand Down Expand Up @@ -506,13 +514,13 @@ permissions: true

### 2. Use the Appropriate Pattern

| Scenario | Pattern |
|----------|---------|
| Public endpoint | `permissions: false` |
| Standard authenticated CRUD | `permissions: true` |
| Need unsafe attrs tracking | `permissions: { unsafeAttrs: [...] }` |
| Complex custom logic | `permissions: async function(frame) {...}` |
| Need pre-processing | `permissions: { before: async function(frame) {...} }` |
| Scenario | Pattern |
| --------------------------- | ------------------------------------------------------ |
| Public endpoint | `permissions: false` |
| Standard authenticated CRUD | `permissions: true` |
| Need unsafe attrs tracking | `permissions: { unsafeAttrs: [...] }` |
| Complex custom logic | `permissions: async function(frame) {...}` |
| Need pre-processing | `permissions: { before: async function(frame) {...} }` |

### 3. Keep Permission Logic Focused

Expand Down Expand Up @@ -696,6 +704,7 @@ Common roles you can assign permissions to:
### Restricting to Administrators Only

To make an endpoint accessible only to administrators (not editors, authors, etc.), only assign permissions to:

- `Administrator`
- `Admin Integration`

Expand Down
3 changes: 3 additions & 0 deletions .agents/skills/add-admin-api-endpoint/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ options: ['include', 'filter', 'page', 'limit', 'order']
```

Can also be a function:

```javascript
options: (frame) => {
return frame.apiType === 'content'
Expand Down Expand Up @@ -152,6 +153,7 @@ validation: {
```

**Global validators** (automatically applied when parameters are present):

- `id` - Must match `/^[a-f\d]{24}$|^1$|me/i`
- `page` - Must be a number
- `limit` - Must be a number or 'all'
Expand All @@ -161,6 +163,7 @@ validation: {
- `order` - Must match `/^[a-z0-9_,. ]+$/i`

For custom validation, use a function:

```javascript
validation(frame) {
if (!frame.data.posts[0].title) {
Expand Down
35 changes: 23 additions & 12 deletions .agents/skills/add-admin-api-endpoint/validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ The api-framework uses a **pipeline-based validation system** where validations
5. Output serialisation

Validation ensures that:

- Required fields are present
- Values are in allowed lists
- Data types are correct (IDs, emails, slugs, etc.)
Expand Down Expand Up @@ -64,6 +65,7 @@ browse: {
```

**When to use:**

- Standard field validation (required, allowed values)
- Most common case for API endpoints

Expand Down Expand Up @@ -102,6 +104,7 @@ add: {
```

**When to use:**

- Complex validation logic
- Cross-field validation
- Conditional validation rules
Expand Down Expand Up @@ -137,6 +140,7 @@ browse: {
Two equivalent syntaxes:

**Object notation:**

```javascript
validation: {
options: {
Expand All @@ -148,6 +152,7 @@ validation: {
```

**Array shorthand:**

```javascript
validation: {
options: {
Expand Down Expand Up @@ -235,6 +240,7 @@ add: {
```

**Request body structure:**

```json
{
"posts": [{
Expand All @@ -247,6 +253,7 @@ add: {
### Root Key Validation

For ADD/EDIT operations, the framework automatically validates:

1. Root key exists (e.g., `posts`, `users`)
2. Root key contains an array with at least one item
3. Required fields exist and are not null
Expand All @@ -257,22 +264,23 @@ For ADD/EDIT operations, the framework automatically validates:

The framework automatically validates common field types using the `@tryghost/validator` package:

| Field Name | Validation Rule | Example Valid Values |
|------------|-----------------|---------------------|
| `id` | MongoDB ObjectId, `1`, or `me` | `507f1f77bcf86cd799439011`, `me` |
| `uuid` | UUID format | `550e8400-e29b-41d4-a716-446655440000` |
| `slug` | URL-safe slug | `my-post-title` |
| `email` | Email format | `user@example.com` |
| `page` | Numeric | `1`, `25` |
| `limit` | Numeric or `all` | `10`, `all` |
| `from` | Date format | `2024-01-15` |
| `to` | Date format | `2024-12-31` |
| `order` | Sort format | `created_at desc`, `title asc` |
| `columns` | Column list | `id,title,created_at` |
| Field Name | Validation Rule | Example Valid Values |
| ---------- | ------------------------------ | -------------------------------------- |
| `id` | MongoDB ObjectId, `1`, or `me` | `507f1f77bcf86cd799439011`, `me` |
| `uuid` | UUID format | `550e8400-e29b-41d4-a716-446655440000` |
| `slug` | URL-safe slug | `my-post-title` |
| `email` | Email format | `user@example.com` |
| `page` | Numeric | `1`, `25` |
| `limit` | Numeric or `all` | `10`, `all` |
| `from` | Date format | `2024-01-15` |
| `to` | Date format | `2024-12-31` |
| `order` | Sort format | `created_at desc`, `title asc` |
| `columns` | Column list | `id,title,created_at` |

### Fields with No Validation

These fields skip validation by default:

- `filter`
- `context`
- `forUpdate`
Expand Down Expand Up @@ -300,6 +308,7 @@ Different HTTP methods have different validation behaviors:
3. Checks required fields are not null

**Error examples:**

- `"No root key ('posts') provided."`
- `"Validation (FieldIsRequired) failed for title"`
- `"Validation (FieldIsInvalid) failed for title"` (when null)
Expand All @@ -318,6 +327,7 @@ Different HTTP methods have different validation behaviors:
### Special Methods

These methods use specific validation behaviors:

- `changePassword()` - Uses ADD rules
- `resetPassword()` - Uses ADD rules
- `setup()` - Uses ADD rules
Expand Down Expand Up @@ -540,6 +550,7 @@ module.exports = {
### Error Types

Validation errors use types from `@tryghost/errors`:

- **ValidationError** - Field validation failed
- **BadRequestError** - Malformed request structure

Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/add-private-feature-flag/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ description: Use when adding a new private (developer experiments) feature flag
# Add Private Feature Flag

## Overview

Adds a new private feature flag to Ghost. Private flags appear in Labs settings under the "Private features" tab, visible only when developer experiments are enabled.

## Steps
Expand All @@ -22,6 +23,7 @@ Adds a new private feature flag to Ghost. Private flags appear in Labs settings
- Review the diff of `ghost/core/test/e2e-api/admin/__snapshots__/config.test.js.snap` to confirm only your new flag was added.

## Notes

- No database migration is needed. Labs flags are stored in a single JSON `labs` setting.
- The flag name must be identical in `labs.js`, `private-features.tsx`, and the snapshot.
- Flags are camelCase strings (e.g. `welcomeEmailDesignCustomization`).
Expand Down
1 change: 1 addition & 0 deletions .agents/skills/commit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Use this skill whenever the user asks you to create a git commit for the current
5. Run `git status --short` after committing and confirm the result.

## Important

- Do not push to remote unless the user explicitly asks
- Keep commits focused and avoid bundling unrelated changes
- If there are no relevant changes, do not create an empty commit
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/create-database-migration/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ accompanies it.
11. Run unit tests in Ghost core, and iterate until they pass: `cd ghost/core && pnpm test:unit`

## Examples

See [examples.md](examples.md) for example migrations.

## Rules

See [rules.md](rules.md) for rules that should always be followed when creating database migrations.
3 changes: 2 additions & 1 deletion .agents/skills/create-database-migration/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Wherever possible, use the utility functions in `ghost/core/core/server/data/mig
## Migration PRs should be as minimal as possible

Migration PRs should contain the minimal amount of code to create the migration. Usually this means it should only include:

- the new migration file
- updates to the schema.js file
- updated schema integrity hash tests
Expand All @@ -30,4 +31,4 @@ Protect against missing data. If a migration crashes, Ghost cannot boot.

## Migrations should log every code path

If we have to debug a migration, we need to know what it actually did. Without logging, that's impossible, so ensure all code paths and early returns contain logging. Note: when using the utility functions, logging is typically handled in the utility function itself, so no additional logging statements are necessary.
If we have to debug a migration, we need to know what it actually did. Without logging, that's impossible, so ensure all code paths and early returns contain logging. Note: when using the utility functions, logging is typically handled in the utility function itself, so no additional logging statements are necessary.
1 change: 1 addition & 0 deletions .agents/skills/format-number/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {formatNumber} from '@tryghost/shade';
## When to use formatNumber

Use `formatNumber()` when rendering any numeric value that is displayed to the user, including:

- Member counts, visitor counts, subscriber counts
- Email engagement metrics (opens, clicks, bounces)
- Revenue amounts (combine with `centsToDollars()` for monetary values)
Expand Down
28 changes: 14 additions & 14 deletions .agents/skills/shade-component-decision/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ Before adding anything to Shade, walk the decision flow and the promotion checkl

## The five layers

| Layer | Lives in | Examples |
|---|---|---|
| **Token** | `theme-variables.css`, `tailwind.theme.css` | `--background`, `--text-base`, `--radius-md` |
| **Primitive** | `src/components/primitives/` | `Stack`, `Inline`, `Box`, `Grid`, `Container`, `Text` |
| **Component** | `src/components/ui/` | `Button`, `Input`, `Dialog`, `Tabs`, `Card` |
| **Recipe** | `src/components/ui/<name>.ts` (no JSX) | `inputSurface` |
| **Pattern** | `src/components/patterns/` | `PageHeader`, `KpiCard`, `Filters`, `GhAreaChart` |
| Layer | Lives in | Examples |
| ------------- | ------------------------------------------- | ----------------------------------------------------- |
| **Token** | `theme-variables.css`, `tailwind.theme.css` | `--background`, `--text-base`, `--radius-md` |
| **Primitive** | `src/components/primitives/` | `Stack`, `Inline`, `Box`, `Grid`, `Container`, `Text` |
| **Component** | `src/components/ui/` | `Button`, `Input`, `Dialog`, `Tabs`, `Card` |
| **Recipe** | `src/components/ui/<name>.ts` (no JSX) | `inputSurface` |
| **Pattern** | `src/components/patterns/` | `PageHeader`, `KpiCard`, `Filters`, `GhAreaChart` |

Plus one additional barrel:

Expand Down Expand Up @@ -54,13 +54,13 @@ Each layer can use anything **below** it. The reverse is forbidden.

## Common misclassifications

| Tempting | Actually | Why |
|---|---|---|
| Add a `variant="kpi"` to `Card` | New Pattern `KpiCard` | Product-specific shape, generic Component shouldn't know about it |
| Add `<MembersFilterBar>` to patterns | Keep local in `apps/admin/src/settings` | Single-surface name |
| Add a one-off class string as a recipe | Inline it in the one component | Recipes are for shared rules across ≥ 2 components |
| Add a `useQuery`-driven `<MembersList>` to patterns | Keep local — patterns are state-free | Bring-your-own state |
| Wrap a `<div className="flex gap-3">` as a new primitive | Use `Inline gap="md"` | Already covered |
| Tempting | Actually | Why |
| -------------------------------------------------------- | --------------------------------------- | ----------------------------------------------------------------- |
| Add a `variant="kpi"` to `Card` | New Pattern `KpiCard` | Product-specific shape, generic Component shouldn't know about it |
| Add `<MembersFilterBar>` to patterns | Keep local in `apps/admin/src/settings` | Single-surface name |
| Add a one-off class string as a recipe | Inline it in the one component | Recipes are for shared rules across ≥ 2 components |
| Add a `useQuery`-driven `<MembersList>` to patterns | Keep local — patterns are state-free | Bring-your-own state |
| Wrap a `<div className="flex gap-3">` as a new primitive | Use `Inline gap="md"` | Already covered |

## When you've decided

Expand Down
14 changes: 7 additions & 7 deletions .agents/skills/shade-dropdown-surface-contract/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ In light mode the three surface tokens (`background` / `surface-elevated` / `sur

## The contract

| Concern | Class |
|---|---|
| Background | `bg-surface-elevated-2` |
| Border | `border border-border/60 dark:border-border/30` |
| Shadow | `shadow-md` baseline. **`DropdownMenuContent` is the exception — it uses `shadow-lg`.** `DropdownMenuSubContent`, `SelectContent`, and `PopoverContent` all use `shadow-md`. |
| Radius | `rounded-md` (component default) |
| Animation | Standard Radix `data-[state=open]:` enter/exit set |
| Concern | Class |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Background | `bg-surface-elevated-2` |
| Border | `border border-border/60 dark:border-border/30` |
| Shadow | `shadow-md` baseline. **`DropdownMenuContent` is the exception — it uses `shadow-lg`.** `DropdownMenuSubContent`, `SelectContent`, and `PopoverContent` all use `shadow-md`. |
| Radius | `rounded-md` (component default) |
| Animation | Standard Radix `data-[state=open]:` enter/exit set |

The shadow split is intentional: the top-level `DropdownMenuContent` opens straight out of an unelevated trigger (a button on the page canvas) and needs the stronger drop. `DropdownMenuSubContent` already pops from inside a floating menu, so it uses the lighter `shadow-md` to avoid double-stacking elevation. `Select` and `Popover` also open from low-elevation triggers but their content sits closer to the trigger surface, so `shadow-md` is enough.

Expand Down
18 changes: 9 additions & 9 deletions .agents/skills/shade-input-surface-recipe/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,15 @@ Available atoms:

## What the recipe owns vs. what you add

| Recipe owns | You add |
|---|---|
| Border (`border-control-border`) | Height, padding |
| Background (`bg-control-surface`) | Typography (`text-control`, `text-sm`) |
| Radius (`rounded-md`) | Layout (`flex`, `items-center`) |
| Transition (`transition-colors`) | Placeholder styling |
| Focus ring (`focus-visible:ring-focus-ring/25`) | Component-specific tweaks |
| Invalid state (`aria-[invalid=true]:border-destructive`) | Icons / slot positioning |
| Disabled (`disabled:opacity-50`) — self only | — |
| Recipe owns | You add |
| -------------------------------------------------------- | -------------------------------------- |
| Border (`border-control-border`) | Height, padding |
| Background (`bg-control-surface`) | Typography (`text-control`, `text-sm`) |
| Radius (`rounded-md`) | Layout (`flex`, `items-center`) |
| Transition (`transition-colors`) | Placeholder styling |
| Focus ring (`focus-visible:ring-focus-ring/25`) | Component-specific tweaks |
| Invalid state (`aria-[invalid=true]:border-destructive`) | Icons / slot positioning |
| Disabled (`disabled:opacity-50`) — self only | — |

## Don't

Expand Down
Loading
Loading