first commit
This commit is contained in:
435
AGENTS.md
Normal file
435
AGENTS.md
Normal file
@@ -0,0 +1,435 @@
|
||||
This file provides guidance to agentic coding tools when working with code in this repository.
|
||||
|
||||
## Project Status
|
||||
|
||||
**Beta.** EmDash is published to npm. All development happens inside this monorepo using `workspace:*` links. See [CONTRIBUTING.md](CONTRIBUTING.md) for the human-readable contributor guide (setup, repo layout, "build your own site" workflow).
|
||||
|
||||
## Repository Structure
|
||||
|
||||
This is a monorepo using pnpm workspaces.
|
||||
|
||||
`CLAUDE.md` is a symlink to `AGENTS.md`. `.opencode/skills` and `.claude/skills` are symlinks to `skills/`. Don't try to sync between them.
|
||||
|
||||
- **Root**: Workspace configuration and shared tooling
|
||||
- **packages/core**: Main `emdash` package - Astro integration and core APIs
|
||||
- **demos/**: Demo applications and examples (`demos/simple/` is the primary dev target)
|
||||
- **templates/**: Starter templates (blog, marketing, portfolio, starter, blank) -- contributors copy these into `demos/` to build their own sites
|
||||
- **docs/**: Public documentation site (Starlight)
|
||||
|
||||
# Rules
|
||||
|
||||
This is a pre-release project. Do not add backwards compatibility or legacy patterns. Do not deprecate -- remove instead. Do not add migration paths.
|
||||
|
||||
**Build for the known future.** If we know we'll need something, build it now. Only defer things where there's genuine uncertainty about whether or how we'll need them. "We'll need it later" is a reason to do it now, not a reason to punt.
|
||||
|
||||
**TDD for bugs.** Write a failing test -> fix the bug -> verify the test passes. A bug without a reproducing test is not fixed.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Before Starting
|
||||
|
||||
1. Run `pnpm --silent lint:json | jq '.diagnostics | length'` and fix any issues. Non-negotiable.
|
||||
|
||||
### During Work
|
||||
|
||||
- Run `pnpm --silent lint:quick` after every edit -- takes less than a second. Returns JSON with stderr redirected to /dev/null, so it won't break parsers. Fix any issues immediately.
|
||||
- Run `pnpm typecheck` (packages) or `pnpm typecheck:demos` (Astro demos) after each round of edits.
|
||||
- Format regularly. pnpm format in the root uses oxfmt with tabs for indentation and is very fast. Don't let formatting pile up.
|
||||
- Commit regularly, and always format and quick lint beforehand.
|
||||
- Update tasks.md when completing tasks. Write a journal entry when starting or finishing significant work, or if you learn anything interesting or useful that you'd like to remember.
|
||||
|
||||
### Before Committing
|
||||
|
||||
You verified linting and types were clean before starting. If they're failing now, your changes caused it -- even if the errors are in files you didn't touch. Don't dismiss failures as "unrelated". Don't assign blame. Just fix them.
|
||||
|
||||
### PR Flow
|
||||
|
||||
1. All tests pass: `pnpm test`
|
||||
2. Full lint suite clean: `pnpm --silent lint:json | jq '.diagnostics | length'`. Returns JSON with stderr piped to /dev/null, so it won't break parsers. Fix any issues.
|
||||
3. Format with `pnpm format` (oxfmt with tabs for indentation, configured in `.prettierrc`).
|
||||
4. Open the PR with the `pr` skill
|
||||
|
||||
### Dev Servers
|
||||
|
||||
Use `bgproc` (not raw process management):
|
||||
|
||||
```bash
|
||||
bgproc start -n devserver -w -- pnpm dev # start and wait for port
|
||||
bgproc stop devserver # stop
|
||||
bgproc logs devserver # view logs
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
EmDash is an Astro-native CMS that stores its schema in the database, not in code.
|
||||
|
||||
### Core Architecture
|
||||
|
||||
- **Schema in the database.** `_emdash_collections` and `_emdash_fields` are the source of truth. Each collection gets a real SQL table (`ec_posts`, `ec_products`) with typed columns -- not EAV.
|
||||
- **Middleware chain** (in order): runtime init -> setup check -> auth -> request context (ALS). Auth middleware handles authentication; individual routes handle authorization.
|
||||
- **Handler layer** (`api/handlers/*.ts`) -- Business logic returns `ApiResponse<T>` (`{ success, data?, error? }`). Route files are thin wrappers that parse input, call handlers, and format responses.
|
||||
- **Storage abstraction** -- `Storage` interface with `upload/download/delete/exists/list/getSignedUploadUrl`. Implementations: `LocalStorage` (dev), `S3Storage` (R2/AWS). Access via `emdash.storage` from locals.
|
||||
|
||||
### Known Quality Patterns
|
||||
|
||||
**Index discipline.** Every content table gets indexes on: `status`, `slug`, `created_at`, `deleted_at`, `scheduled_at` (partial -- `WHERE scheduled_at IS NOT NULL`), `live_revision_id`, `draft_revision_id`, `author_id`, `primary_byline_id`, `updated_at`, `locale`, `translation_group`. Foreign key columns always get an index. Naming: `idx_{table}_{column}` for single-column, `idx_{table}_{purpose}` for multi-column.
|
||||
|
||||
**API envelope consistency.** Handlers return `ApiResponse<T>` wrapping data in `{ success, data }`. List endpoints return `{ items, nextCursor? }` inside `data`. The admin client's `parseApiResponse` unwraps `body.data`. Be aware of this layering when adding new endpoints.
|
||||
|
||||
## Commands
|
||||
|
||||
### Root-level commands (run from repository root):
|
||||
|
||||
- `pnpm build` - Build all packages
|
||||
- `pnpm test` - Run tests for all packages
|
||||
- `pnpm check` - Run type checking and linting for all packages
|
||||
- `pnpm format` - Format code using oxfmt
|
||||
|
||||
### Package-level commands (run within individual packages):
|
||||
|
||||
- `pnpm build` - Build the package using tsdown (ESM + DTS output)
|
||||
- `pnpm dev` - Watch mode for development
|
||||
- `pnpm test` - Run vitest tests
|
||||
- `pnpm check` - Run publint and @arethetypeswrong/cli checks
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
| ----------------------------------- | ----------------------------------------------------- |
|
||||
| `src/live.config.ts` | Collection schemas + admin config (user's site) |
|
||||
| `src/emdash-runtime.ts` | Central runtime; orchestrates DB, plugins, storage |
|
||||
| `src/schema/registry.ts` | Manages `ec_*` table creation/modification |
|
||||
| `src/database/migrations/runner.ts` | StaticMigrationProvider; register new migrations here |
|
||||
| `src/plugins/manager.ts` | Loads and orchestrates trusted plugins |
|
||||
|
||||
## Code Patterns
|
||||
|
||||
### Database: Never Interpolate Into SQL
|
||||
|
||||
Kysely is the query builder. Use it properly:
|
||||
|
||||
- **Never** use `sql.raw()` with string interpolation or template literals containing variables.
|
||||
- **Never** build SQL strings with `+` or backtick interpolation and pass them to `sql.raw()`.
|
||||
- For **values**, use Kysely's `sql` tagged template: `` sql`SELECT * FROM t WHERE id = ${id}` `` -- interpolated values are automatically parameterized.
|
||||
- For **identifiers** (table/column names), use `sql.ref()` which quotes them safely.
|
||||
- If you absolutely must use `sql.raw()` for dynamic identifiers, validate them first with `validateIdentifier()` from `database/validate.ts` which asserts `/^[a-z][a-z0-9_]*$/`.
|
||||
- The `json_extract(data, '$.${field}')` pattern is particularly dangerous -- always validate `field` before interpolation.
|
||||
|
||||
```typescript
|
||||
// WRONG -- SQL injection via string interpolation
|
||||
const query = `SELECT * FROM ${table} WHERE name = '${name}'`;
|
||||
await sql.raw(query).execute(db);
|
||||
|
||||
// WRONG -- field name interpolated into sql.raw()
|
||||
return sql.raw(`json_extract(data, '$.${field}')`);
|
||||
|
||||
// RIGHT -- parameterized value
|
||||
await sql`SELECT * FROM ${sql.ref(table)} WHERE name = ${name}`.execute(db);
|
||||
|
||||
// RIGHT -- validated identifier in raw SQL
|
||||
validateIdentifier(field);
|
||||
return sql.raw(`json_extract(data, '$.${field}')`);
|
||||
```
|
||||
|
||||
### API Routes: Use Shared Utilities
|
||||
|
||||
All API routes under `astro/routes/api/` must follow these patterns:
|
||||
|
||||
**Error responses** -- use `apiError()` from `api/error.ts`:
|
||||
|
||||
```typescript
|
||||
// WRONG -- inline JSON.stringify with ad-hoc shape
|
||||
return new Response(JSON.stringify({ error: "Not found" }), { status: 404 });
|
||||
|
||||
// RIGHT -- consistent shape: { error: { code, message } }
|
||||
return apiError("NOT_FOUND", "Content not found", 404);
|
||||
```
|
||||
|
||||
**Catch blocks** -- use `handleError()`, never expose `error.message` to clients:
|
||||
|
||||
```typescript
|
||||
// WRONG -- leaks internal error details
|
||||
catch (error) {
|
||||
return new Response(JSON.stringify({
|
||||
error: error instanceof Error ? error.message : "Unknown error"
|
||||
}), { status: 500 });
|
||||
}
|
||||
|
||||
// RIGHT -- logs internally, returns generic message
|
||||
catch (error) {
|
||||
return handleError(error, "Failed to update content", "CONTENT_UPDATE_ERROR");
|
||||
}
|
||||
```
|
||||
|
||||
**Input validation** -- use `parseBody()` / `parseQuery()` from `api/parse.ts`, never use `as` casts on `request.json()`:
|
||||
|
||||
```typescript
|
||||
// WRONG -- no runtime validation, malformed input reaches the database
|
||||
const body = (await request.json()) as CreateContentInput;
|
||||
|
||||
// RIGHT -- Zod validation, returns 400 on failure
|
||||
const body = await parseBody(request, createContentSchema);
|
||||
```
|
||||
|
||||
**Initialization checks** -- use a consistent message:
|
||||
|
||||
```typescript
|
||||
if (!emdash) return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
||||
```
|
||||
|
||||
**Handler results** -- when using the handler layer (`api/handlers/*.ts`), always unwrap consistently:
|
||||
|
||||
```typescript
|
||||
const result = await handler.handleContentGet(collection, id);
|
||||
if (!result.success) {
|
||||
return apiError(result.error.code, result.error.message, mapErrorToStatus(result.error.code));
|
||||
}
|
||||
return Response.json(result.data);
|
||||
```
|
||||
|
||||
### API Routes: Authorization
|
||||
|
||||
Every route that modifies state must check authorization. The auth middleware only checks authentication (is the user logged in); individual routes must check roles:
|
||||
|
||||
```typescript
|
||||
import { requireRole, Role } from "../../auth/permissions.js";
|
||||
|
||||
// At the top of any state-changing handler:
|
||||
const roleError = requireRole(user, Role.EDITOR);
|
||||
if (roleError) return roleError;
|
||||
```
|
||||
|
||||
Minimum roles:
|
||||
|
||||
- **ADMIN**: settings, schema, plugins, user management, imports, search rebuild
|
||||
- **EDITOR**: all content CRUD, media, taxonomies, menus, widgets, publish/unpublish
|
||||
- **AUTHOR**: own content CRUD, media upload
|
||||
- **CONTRIBUTOR**: own content create/edit (no publish), media upload
|
||||
|
||||
### API Routes: CSRF Protection
|
||||
|
||||
All state-changing endpoints (POST/PUT/DELETE) require the `X-EmDash-Request: 1` header, enforced by auth middleware. The admin UI and visual editing client send this header automatically. Do not add GET handlers for state-changing operations.
|
||||
|
||||
### Pagination
|
||||
|
||||
All list endpoints must use cursor-based pagination with a consistent shape:
|
||||
|
||||
```typescript
|
||||
// Return type for all list queries
|
||||
interface FindManyResult<T> {
|
||||
items: T[];
|
||||
nextCursor?: string;
|
||||
}
|
||||
```
|
||||
|
||||
- Use `encodeCursor(orderValue, id)` / `decodeCursor(cursor)` utilities.
|
||||
- Default limit: 50. Maximum limit: 100. Always clamp.
|
||||
- The response array key is always `items` (not `results`, not a bare array).
|
||||
- Never return a bare array from a list endpoint -- always wrap in `{ items, nextCursor? }`.
|
||||
|
||||
### Adding Database Tables or Columns
|
||||
|
||||
When creating tables or adding columns queried in WHERE or ORDER BY clauses, add indexes. Check existing patterns in `database/migrations/` and `schema/registry.ts`. Foreign key columns should always have an index.
|
||||
|
||||
Index naming: `idx_{table}_{column}` for single-column, `idx_{table}_{purpose}` for multi-column. Content tables get standard indexes on `status`, `slug`, `created_at`, `deleted_at`, `author_id`, and all foreign key columns.
|
||||
|
||||
### Migrations
|
||||
|
||||
Migrations live in `packages/core/src/database/migrations/`. Conventions:
|
||||
|
||||
- **Naming:** `NNN_descriptive_name.ts` -- zero-padded 3-digit sequential number.
|
||||
- **Exports:** Each migration exports `up(db: Kysely<unknown>)` and `down(db: Kysely<unknown>)`.
|
||||
- **System tables** use Kysely's schema builder (`db.schema.createTable(...)`).
|
||||
- **Dynamic content tables** (`ec_*`) use `sql` tagged templates with `sql.ref()` for identifiers.
|
||||
- **Column types:** SQLite types -- `"text"`, `"integer"`, `"real"`, `"blob"`. Booleans are `"integer"` with `defaultTo(0)`. Timestamps are `"text"` with ``defaultTo(sql`(datetime('now'))`)``. IDs are `"text"` primary keys (ULIDs from `ulidx`).
|
||||
- **Index naming:** `idx_{table}_{column}` for single-column, `idx_{table}_{purpose}` for multi-column.
|
||||
- **Foreign keys** must always have an accompanying index.
|
||||
- **Registration:** Migrations are statically imported in `database/runner.ts` and added to the `StaticMigrationProvider`. They are NOT auto-discovered -- this is required for Workers bundler compatibility. When adding a migration: (1) create the file, (2) add a static import in `runner.ts`, (3) add it to `getMigrations()`.
|
||||
- **Multi-table migrations:** When altering all content tables, query `_emdash_collections` to discover `ec_*` tables and loop. See `013_scheduled_publishing.ts` for the pattern.
|
||||
|
||||
### API Route Structure
|
||||
|
||||
Route files live in `packages/core/src/astro/routes/api/`. Conventions:
|
||||
|
||||
- Every route file starts with `export const prerender = false;`.
|
||||
- Handlers are named exports: `export const GET: APIRoute`, `export const POST: APIRoute`, etc.
|
||||
- Handlers destructure from the Astro context: `({ params, request, url, locals })`.
|
||||
- Access the CMS runtime via `const { emdash } = locals;`.
|
||||
- Access the user via `const user = (locals as { user?: User }).user;`.
|
||||
- URL structure mirrors file structure: `content/[collection]/index.ts` for list/create, `content/[collection]/[id].ts` for get/update/delete, with sub-actions as siblings: `[id]/publish.ts`, `[id]/schedule.ts`.
|
||||
- **Never** add GET handlers for state-changing operations.
|
||||
|
||||
### Handler Layer
|
||||
|
||||
Handlers in `api/handlers/*.ts` contain business logic. Routes should be thin wrappers.
|
||||
|
||||
- Handlers are standalone async functions (not class methods).
|
||||
- First parameter is always `db: Kysely<Database>`, followed by route-specific params.
|
||||
- Always return `ApiResponse<T>` -- the `{ success, data?, error? }` discriminated union from `api/types.ts`.
|
||||
- Entire body wrapped in try/catch. Errors return `{ success: false, error: { code, message } }`.
|
||||
- Error codes are `SCREAMING_SNAKE_CASE`: `NOT_FOUND`, `VALIDATION_ERROR`, `CONTENT_CREATE_ERROR`, etc.
|
||||
|
||||
### Admin UI: API Error Handling
|
||||
|
||||
All admin API functions use `throwResponseError()` from `lib/api/client.ts` to surface server error messages to the user. Never throw a generic error when the response body contains a message.
|
||||
|
||||
```typescript
|
||||
import { apiFetch, throwResponseError } from "./client.js";
|
||||
|
||||
// WRONG -- loses the server's error message
|
||||
if (!response.ok) throw new Error("Failed to create term");
|
||||
|
||||
// WRONG -- manually parsing what throwResponseError already does
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error?.message || "Failed to create term");
|
||||
}
|
||||
|
||||
// RIGHT -- parses { error: { message } } body, falls back to generic message
|
||||
if (!response.ok) await throwResponseError(response, "Failed to create term");
|
||||
```
|
||||
|
||||
### Admin UI: Confirmation Dialogs
|
||||
|
||||
Use `ConfirmDialog` from `components/ConfirmDialog.tsx` for all confirmation modals (delete, disable, demote, etc.). Pass `mutation.error` directly -- don't manage error state manually.
|
||||
|
||||
```typescript
|
||||
import { ConfirmDialog } from "./ConfirmDialog.js";
|
||||
|
||||
<ConfirmDialog
|
||||
open={!!deleteSlug}
|
||||
onClose={() => { setDeleteSlug(null); deleteMutation.reset(); }}
|
||||
title="Delete Section?"
|
||||
description="This will permanently delete the section."
|
||||
confirmLabel="Delete"
|
||||
pendingLabel="Deleting..."
|
||||
isPending={deleteMutation.isPending}
|
||||
error={deleteMutation.error}
|
||||
onConfirm={() => deleteMutation.mutate(deleteSlug)}
|
||||
/>
|
||||
```
|
||||
|
||||
### Admin UI: Inline Dialog Errors
|
||||
|
||||
For form dialogs and other cases where `ConfirmDialog` doesn't fit, use `DialogError` and `getMutationError()` from `components/DialogError.tsx`:
|
||||
|
||||
```typescript
|
||||
import { DialogError, getMutationError } from "./DialogError.js";
|
||||
|
||||
// In JSX -- renders nothing when message is null
|
||||
<DialogError message={getMutationError(createMutation.error)} />
|
||||
|
||||
// With local error state fallback (e.g. client-side validation)
|
||||
<DialogError message={localError || getMutationError(mutation.error)} />
|
||||
```
|
||||
|
||||
Don't duplicate the error banner styling inline -- always use `DialogError`.
|
||||
|
||||
### Import Conventions
|
||||
|
||||
- **Internal imports** always use `.js` extensions (ESM requirement):
|
||||
```typescript
|
||||
import { ContentRepository } from "../../database/repositories/content.js";
|
||||
```
|
||||
- **Type-only imports** must use `import type` (enforced by `verbatimModuleSyntax: true`):
|
||||
```typescript
|
||||
import type { Kysely } from "kysely";
|
||||
```
|
||||
- **Package imports** do not use extensions: `import { sql } from "kysely"`.
|
||||
- **Virtual modules** use `// @ts-ignore` comment:
|
||||
```typescript
|
||||
// @ts-ignore - virtual module
|
||||
import virtualConfig from "virtual:emdash/config";
|
||||
```
|
||||
- **Barrel files** (`index.ts`) re-export from sub-modules. Separate `export type { ... }` from value exports.
|
||||
|
||||
### Environment Gating
|
||||
|
||||
- **Dev-only endpoints** must check `import.meta.env.DEV` and return 403 if false. This is a compile-time constant -- it cannot be spoofed at runtime.
|
||||
- **Never** use `process.env.NODE_ENV` -- always use `import.meta.env.DEV` or `import.meta.env.PROD` (Vite/Astro standard).
|
||||
- **Secrets** follow the pattern: `import.meta.env.EMDASH_X || import.meta.env.X || ""` -- check prefixed name first, then generic, then fallback.
|
||||
|
||||
### Cloudflare Env
|
||||
|
||||
To access the Cloudflare `env` object, import it directly from `"cloudflare:workers"` -- no need to access it from the context in a handler. This is a virtual module that resolves to the correct bindings for the current environment, whether that's a Worker or a local dev environment.
|
||||
|
||||
Do not manually type the Cloudflare Env object. When in a Worker context, run `pnpm wrangler types` to generate `worker-configuration.d.ts` with the correct bindings for the current environment. This includes types for bindings in wrangler.jsonc as well as secrets in `.dev.vars`. Regenerate it if you edit the bindings. Ensure it is referenced in `tsconfig.json` under `include` and then the types will be available globally.
|
||||
|
||||
If not working in a Worker context, but in a library that will be used in a Worker, install `@cloudflare/workers-types` and reference it in `tsconfig.json` under `compilerOptions.types`. This will allow you to use Cloudflare-specific types like `R2Bucket` and `D1Database` in your code.
|
||||
|
||||
### Content Table Lifecycle
|
||||
|
||||
Dynamic content tables are managed by `SchemaRegistry` in `schema/registry.ts`:
|
||||
|
||||
- **Table names:** `ec_{collection_slug}` (e.g., `ec_posts`). System tables: `_emdash_{name}`.
|
||||
- **Slug validation:** `/^[a-z][a-z0-9_]*$/`, max 63 chars. Checked against `RESERVED_COLLECTION_SLUGS` and `RESERVED_FIELD_SLUGS`.
|
||||
- **Standard columns:** Every content table gets `id`, `slug`, `status`, `author_id`, `created_at`, `updated_at`, `published_at`, `scheduled_at`, `deleted_at`, `version`, `live_revision_id`, `draft_revision_id`. User-defined field columns are added via `ALTER TABLE`.
|
||||
- **Field type mapping:** `FIELD_TYPE_TO_COLUMN` maps: string/text/datetime/image/reference -> TEXT, number -> REAL, integer/boolean -> INTEGER, portableText/json -> JSON.
|
||||
- **Orphan discovery:** `discoverOrphanedTables()` finds `ec_*` tables without matching `_emdash_collections` entries. This is used for recovering from crashes during schema changes.
|
||||
|
||||
### Testing
|
||||
|
||||
- **Framework:** vitest. Tests in `packages/core/tests/`.
|
||||
- **Database:** Tests use real in-memory SQLite via `better-sqlite3` + Kysely. No DB mocking.
|
||||
- **Utilities:** `tests/utils/test-db.ts` provides `createTestDatabase()`, `setupTestDatabase()` (with migrations), and `setupTestDatabaseWithCollections()` (with standard post/page collections).
|
||||
- **Structure:** `tests/unit/` for unit, `tests/integration/` for integration (real DB), `tests/e2e/` for Playwright. Test files mirror source structure.
|
||||
- **Lifecycle:** Each test gets a fresh in-memory DB in `beforeEach`, destroyed in `afterEach`.
|
||||
|
||||
### URL and Redirect Handling
|
||||
|
||||
When accepting redirect URLs from query params or request bodies:
|
||||
|
||||
- Validate the URL starts with `/` (relative path only).
|
||||
- Reject URLs starting with `//` (protocol-relative -- would redirect to external hosts).
|
||||
- HTML-escape any URL values before interpolating into HTML responses.
|
||||
- Prefer server-side `Response.redirect()` over HTML `<meta http-equiv="refresh">`.
|
||||
|
||||
## Toolchain
|
||||
|
||||
- **pnpm** -- package manager
|
||||
- **tsdown** -- TypeScript builds (ESM + DTS)
|
||||
- **vitest** -- testing
|
||||
- **oxfmt** -- code formatting (tabs for indentation, configured in `.prettierrc`). All source files use tabs, not spaces.
|
||||
|
||||
## TypeScript Configuration
|
||||
|
||||
- Target: ES2022
|
||||
- Module: preserve (for bundler compatibility)
|
||||
- Strict mode with `noUncheckedIndexedAccess`, `noImplicitOverride`
|
||||
|
||||
## Dev Bypass for Browser Testing
|
||||
|
||||
EmDash uses passkey authentication which cannot be automated in browser tests. Two dev-only endpoints are available to bypass authentication:
|
||||
|
||||
### Setup Bypass
|
||||
|
||||
Skips the setup wizard, runs migrations, creates a dev admin user, and establishes a session:
|
||||
|
||||
```
|
||||
GET /_emdash/api/setup/dev-bypass?redirect=/_emdash/admin
|
||||
```
|
||||
|
||||
### Auth Bypass
|
||||
|
||||
Creates a session for the dev admin user (assumes setup is already complete):
|
||||
|
||||
```
|
||||
GET /_emdash/api/auth/dev-bypass?redirect=/_emdash/admin
|
||||
```
|
||||
|
||||
### Usage in Agent Browser
|
||||
|
||||
When testing the admin UI with agent-browser, navigate to the setup bypass URL first:
|
||||
|
||||
```typescript
|
||||
await page.goto("http://localhost:4321/_emdash/api/setup/dev-bypass?redirect=/_emdash/admin");
|
||||
```
|
||||
|
||||
This will:
|
||||
|
||||
1. Run database migrations
|
||||
2. Create a dev admin user (`dev@emdash.local`)
|
||||
3. Set up a session cookie
|
||||
4. Redirect to the admin dashboard
|
||||
|
||||
**Note**: These endpoints only work when `import.meta.env.DEV` is true. They return 403 in production.
|
||||
Reference in New Issue
Block a user