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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions claude-plugins/opensaas-migration/agents/migration-assistant.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,22 @@ Change `session.data.id` → `session.userId` in access control functions.

The subagent will search the project, edit all files, and report what it changed.

### Step 8: Run generation and validate
### Step 10: Set up Admin UI — ask the user, then delegate to subagent

**Do not do this yourself.** Ask the user two questions:

1. "Would you like to set up the OpenSaaS Stack Admin UI in this project? It provides a full CRUD interface for all your lists out of the box."
2. "What path should the admin UI be mounted at? (default: `/admin`)"

If the user wants the admin UI:

> Invoke `setup-admin-ui` with: "Project root: /path/to/project. Admin path: /admin (or whatever they chose). Auth enabled: yes/no (based on whether authPlugin was detected in their config)."

The subagent will install `@opensaas/stack-ui`, create `app/{path}/[[...{segment}]]/page.tsx`, and report next steps.

If the user declines, skip this step and proceed to validation.

### Step 11: Run generation and validate

```bash
pnpm opensaas generate # Generates prisma schema
Expand Down Expand Up @@ -410,6 +425,7 @@ Your job is to plan and coordinate the migration, not to do all the editing your
- context.graphql/context.query migration → `migrate-context-calls` skill
- Image/file field migration (config + SQL) → `migrate-image-fields` skill
- Document field migration (→ tiptap) → `migrate-document-fields` skill
- Admin UI setup (ask first, then delegate) → `setup-admin-ui` skill

### When the user says "help me migrate" or similar:

Expand Down Expand Up @@ -446,7 +462,17 @@ After assessing, show the user a numbered list of exactly what will change and w
- **If virtual fields**: invoke `migrate-virtual-fields` with config path and virtual field code
- **If context.graphql/context.query**: invoke `migrate-context-calls` with project root path

**Phase 5 — Validate:**
**Phase 5 — Admin UI:**

Ask the user two questions (both required before delegating):

1. "Would you like to set up the OpenSaaS Stack Admin UI? It gives you a full CRUD interface for all your lists out of the box, at a path you choose."
2. "What path should it be mounted at?" (default: `/admin`)

If yes → **invoke `setup-admin-ui`** with: project root, chosen admin path, and whether `authPlugin` is in their config.
If no → skip and go to Phase 6.

**Phase 6 — Validate:**

- Run `pnpm opensaas generate` and report any errors
- If image/file fields were found, remind the user to run the SQL migration script BEFORE `prisma db push`
Expand Down Expand Up @@ -515,4 +541,5 @@ Guide them through:
2. Run `pnpm opensaas generate`
3. Run `npx prisma db push`
4. Start dev server: `pnpm dev`
5. Visit the admin UI at `http://localhost:3000/admin`
5. If Admin UI was set up: visit `http://localhost:3000/{adminPath}` (e.g. `http://localhost:3000/admin`)
6. If Admin UI was skipped: mention that they can set it up any time — see https://stack.opensaas.au/admin-ui
174 changes: 174 additions & 0 deletions claude-plugins/opensaas-migration/skills/setup-admin-ui/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
---
name: setup-admin-ui
description: Set up the OpenSaaS Stack Admin UI in an existing Next.js App Router project. Invoke as a forked subagent after migration is complete, passing the project root, desired admin path, and whether auth is enabled.
context: fork
agent: general-purpose
---

Set up the OpenSaaS Stack Admin UI in the Next.js project described below.

$ARGUMENTS

## What This Skill Does

1. Installs `@opensaas/stack-ui` if not already a dependency
2. Creates the catch-all admin route page at `app/{adminPath}/[[...{segmentName}]]/page.tsx`
3. Validates the generated file works with the project structure

## Step 1 — Check if `@opensaas/stack-ui` Is Already Installed

Read `package.json` in the project root. If `@opensaas/stack-ui` is **not** in `dependencies`, install it:

```bash
# Detect package manager
# - If pnpm-lock.yaml exists → pnpm add @opensaas/stack-ui
# - If yarn.lock exists → yarn add @opensaas/stack-ui
# - Otherwise → npm install @opensaas/stack-ui
```

Check existing versions of `@opensaas/stack-core` in `package.json` and install `@opensaas/stack-ui` at the **same version** to avoid mismatches.

## Step 2 — Determine the Admin Path

The admin path comes from `$ARGUMENTS` (e.g. `/admin`, `/dashboard/admin`).

- **Route path**: `app/{adminPath}/[[...{segmentName}]]/page.tsx`
- For `/admin` → `app/admin/[[...admin]]/page.tsx`
- For `/dashboard/admin` → `app/dashboard/admin/[[...admin]]/page.tsx`
- For `/cms` → `app/cms/[[...cms]]/page.tsx`
- **Segment name** (used in both the directory name and the `params.{segmentName}` reference): the last path segment (e.g. `admin`, `cms`)
- **`basePath`** prop on `<AdminUI>`: the full admin path (e.g. `/admin`, `/dashboard/admin`)

Create all intermediate directories as needed.

## Step 3 — Determine if Auth Is Enabled

Check whether auth is configured:

1. Look in `opensaas.config.ts` for `authPlugin` usage
2. Check for a `lib/auth.ts` or `lib/auth/index.ts` that exports `getSession`

This determines which page template to use.

## Step 4 — Create the Admin Page

### Template A — Without Auth

Use this when auth is NOT configured:

```typescript
import { AdminUI } from '@opensaas/stack-ui'
import type { ServerActionInput } from '@opensaas/stack-ui/server'
import { getContext, config } from '@/.opensaas/context'

async function serverAction(props: ServerActionInput) {
'use server'
const context = await getContext()
return await context.serverAction(props)
}

interface AdminPageProps {
params: Promise<{ {SEGMENT_NAME}?: string[] }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}

export default async function AdminPage({ params, searchParams }: AdminPageProps) {
const resolvedParams = await params
const resolvedSearchParams = await searchParams
return (
<AdminUI
context={await getContext()}
config={await config}
params={resolvedParams.{SEGMENT_NAME}}
searchParams={resolvedSearchParams}
basePath="{ADMIN_PATH}"
serverAction={serverAction}
/>
)
}
```

### Template B — With Auth

Use this when `authPlugin` is detected and `getSession` is available:

```typescript
import { AdminUI } from '@opensaas/stack-ui'
import type { ServerActionInput } from '@opensaas/stack-ui/server'
import { getContext, config } from '@/.opensaas/context'
import { getSession } from '@/lib/auth'

async function serverAction(props: ServerActionInput) {
'use server'
const context = await getContext({ session: await getSession() })
return await context.serverAction(props)
}

interface AdminPageProps {
params: Promise<{ {SEGMENT_NAME}?: string[] }>
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}

export default async function AdminPage({ params, searchParams }: AdminPageProps) {
const resolvedParams = await params
const resolvedSearchParams = await searchParams
const session = await getSession()
if (!session) {
return (
<div className="p-8">
<div className="bg-destructive/10 border border-destructive text-destructive rounded-lg p-6">
<h2 className="text-lg font-semibold mb-2">Access Denied</h2>
<p>You must be logged in to access the admin interface.</p>
</div>
</div>
)
}
return (
<AdminUI
context={await getContext(session)}
config={await config}
params={resolvedParams.{SEGMENT_NAME}}
searchParams={resolvedSearchParams}
basePath="{ADMIN_PATH}"
serverAction={serverAction}
/>
)
}
```

Replace `{SEGMENT_NAME}` with the last segment of the admin path (e.g. `admin`) and `{ADMIN_PATH}` with the full path (e.g. `/admin`).

**Important**: If `getSession` is not at `@/lib/auth`, check for it at its actual location (e.g. `@/lib/auth/index`, `@/app/lib/auth`) and use the correct import path.

## Step 5 — Check for Missing `.opensaas/context`

The admin page imports from `@/.opensaas/context`. This file is generated by `pnpm opensaas generate` (or `pnpm generate`). If it doesn't exist yet:

- **Do not create it manually** — it's generated from `opensaas.config.ts`
- Remind the user to run `pnpm generate` (or `pnpm opensaas generate`) before starting the dev server

## Step 6 — Report What Was Done

Report to the user:

```
✓ Admin UI set up at: {adminPath}
✓ File created: app/{adminPath}/[[...{segmentName}]]/page.tsx
✓ Auth-aware: yes/no
✓ @opensaas/stack-ui: already installed / installed at version X.Y.Z

Next steps:
1. Run `pnpm generate` to generate the .opensaas/context.ts file (if not already done)
2. Run `pnpm dev` to start the dev server
3. Visit http://localhost:3000{adminPath} to access the admin UI

Docs: https://stack.opensaas.au/admin-ui
```

## Notes

- The `[[...{segmentName}]]` catch-all segment handles all admin routes: the list view, item detail, create, and edit pages — all from one file.
- The `basePath` prop must match exactly the URL path where the admin is mounted.
- The `serverAction` wrapper function is required — it provides the server action bridge between the client-side admin UI components and the database context.
- If the user is using a custom `getSession` approach (not from `@opensaas/stack-auth`), the auth template still works — just update the import path and the session shape passed to `getContext`.
- If Tailwind CSS is not configured in the project, mention that `@opensaas/stack-ui` uses Tailwind for styling and link to the docs for setup: https://stack.opensaas.au/admin-ui#tailwind
Loading