FlowPanel ships a small CLI in the `@flowpanel/cli` package. Bootstrap a
project with `pnpm dlx @flowpanel/cli init` — no install needed up front.
`init` adds `@flowpanel/cli` as a devDependency, so every later command
runs as `pnpm flowpanel <command>`. The shipped subcommands are `init`,
`migrate`, `doctor`, `eject`, `dev`, and `new`.

The CLI detects your package manager from the lockfile and uses it
throughout — `init` installs with it, `dev` launches Next.js through its
runner, and every "run this next" hint is printed in its syntax. The samples
below use pnpm; substitute your own.

**Usage:** `flowpanel init [options]`

Initialize FlowPanel in this project

| Option | Description | Default |
| --- | --- | --- |
| `--yes` | Accept detected defaults without prompting (CI mode) | — |
| `--dry-run` | Print the filesystem plan without writing or installing | — |
| `--json` | Emit machine-readable JSON (implies --yes) | — |
| `--path <url>` | Mount the admin at a static URL, for example /ops/admin | — |
| `--db <module>` | Local module exporting your db or prisma client | — |
| `--schema <module>` | Local module exporting your Drizzle schema | — |
| `--auth <module>` | Local module exporting getSession(request) | — |
| `--dev-auth` | Explicitly use an open development-only admin identity | — |

**Usage:** `flowpanel migrate [options]`

Apply SQL migrations from flowpanel/migrations/

| Option | Description | Default |
| --- | --- | --- |
| `--dry-run` | Print migrations that would be applied without running them | — |
| `--json` | Emit machine-readable JSON | — |

**Usage:** `flowpanel doctor [options]`

Check FlowPanel health and wiring

| Option | Description | Default |
| --- | --- | --- |
| `--fix` | Auto-fix missing route files from templates | — |
| `--dry-run` | Show fixes without writing (use with --fix) | — |
| `--json` | Emit machine-readable JSON | — |
| `--path <url>` | Resolved admin URL when the config uses a dynamic path | — |

**Usage:** `flowpanel eject [options] <target> [name]`

Eject a FlowPanel piece into your app/admin folder. Targets: resource <name>, dashboard <path>, layout.

| Argument | Status |
| --- | --- |
| `<target>` | required |
| `[name]` | optional |

| Option | Description | Default |
| --- | --- | --- |
| `--force` | Overwrite files if they already exist | — |
| `--dry-run` | Print the filesystem plan without writing | — |
| `--json` | Emit machine-readable JSON | — |

**Usage:** `flowpanel dev [options]`

Run Next.js dev server (and bull-board if REDIS_URL is set)

| Option | Description | Default |
| --- | --- | --- |
| `--port <port>` | Next.js dev port | `3000` |
| `--no-board` | Skip the bull-board server even if REDIS_URL is set | — |

**Usage:** `flowpanel new [options] <resource>`

Add a resource(...) entry to flowpanel.config.ts

| Argument | Status |
| --- | --- |
| `<resource>` | required |

| Option | Description | Default |
| --- | --- | --- |
| `--table <expr>` | First argument expression (default: schema.<resource>) | — |
| `--kind <kind>` | Adapter kind: drizzle (default) or prisma | `drizzle` |
| `--dry-run` | Print the filesystem plan without writing | — |
| `--json` | Emit machine-readable JSON | — |

Every command supports `--help`; the root command supports `--version`.
Success exits `0`, validation or execution failures exit non-zero, and fatal
errors show a concise message. Set `FLOWPANEL_DEBUG=1` when you also need the
underlying stack trace.

Mutating commands build a complete filesystem plan before the first write.
`--dry-run` prints that plan; `--json` emits a machine-readable result for CI.
Writes use atomic replacement and a rollback journal, so a later failure
restores earlier files instead of leaving a half-written scaffold. Existing
files with different content are reported as conflicts, not overwritten.

## flowpanel init

**Usage:** `flowpanel init [options]`

Initialize FlowPanel in this project

| Option | Description | Default |
| --- | --- | --- |
| `--yes` | Accept detected defaults without prompting (CI mode) | — |
| `--dry-run` | Print the filesystem plan without writing or installing | — |
| `--json` | Emit machine-readable JSON (implies --yes) | — |
| `--path <url>` | Mount the admin at a static URL, for example /ops/admin | — |
| `--db <module>` | Local module exporting your db or prisma client | — |
| `--schema <module>` | Local module exporting your Drizzle schema | — |
| `--auth <module>` | Local module exporting getSession(request) | — |
| `--dev-auth` | Explicitly use an open development-only admin identity | — |

Detect your stack (Next.js, Drizzle/Prisma, auth helper), pick the
matching config template, install `@flowpanel/kit` + `@flowpanel/cli`,
and scaffold the wiring files. Both Drizzle and Prisma are supported in a
single command; the CLI picks based on your installed dependencies
(Drizzle wins if both are present).

The stylesheet lands at `styles/admin.css`, or at `src/styles/admin.css` when
the project keeps its source under `src/` — the CLI matches your layout rather
than always writing to the repo root.

```text
◇  Detected stack ─────────────────────────────────────╮
│  Next.js 16.3.1 · TypeScript · Drizzle · Tailwind 4  │
├──────────────────────────────────────────────────────╯
◇  7 written
◇  Wrote ─────────────────────────────────────────────────╮
│  flowpanel.config.ts                                    │
│  app/admin/[[...slug]]/page.tsx                         │
│  app/api/flowpanel/[...route]/route.ts                  │
│  app/api/flowpanel/stream/route.ts                      │
│  styles/admin.css                                       │
│  flowpanel/migrations/0001_init.sql                     │
│  app/admin/layout.tsx (imports the admin stylesheet)    │
├─────────────────────────────────────────────────────────╯
◇  Installed @flowpanel/kit, @flowpanel/cli
```

What varies:

- **`src/`** — every `app/` path becomes `src/app/`, and `styles/admin.css`
  becomes `src/styles/admin.css`, when the project uses a `src/` layout.
- **Admin mount** — `/admin` by default, `/flowpanel` when `/admin` is occupied,
  or an explicit `--path /ops/admin`. Route groups are included in collision checks;
  a busy fallback stops installation before any writes.
- **Admin layout** — imports the stylesheet under the chosen route. The existing
  root layout is never patched. Without a shared root layout, the admin segment
  receives its own root layout. That layout reads the `x-nonce` request header and
  forwards it to `ThemeScript`, so the pre-hydration theme script survives a strict
  `script-src` policy — but only when your own middleware sets `x-nonce` on the
  request and emits the matching `script-src 'nonce-…'` header. FlowPanel does not
  write that middleware, and with no `x-nonce` present the header read returns
  `null` and the script is rendered without a nonce, exactly as before. Reading
  `headers()` makes the admin segment dynamically rendered; it already was, because
  `auth.session` reads the request. Existing host theme/CSP integration is left to
  the application; `ThemeScript` accepts its nonce.
- **Authentication** — `init` reads `package.json` for `better-auth`, `next-auth`,
  `@clerk/nextjs` or `lucia` and names what it recognised in the detected-stack
  note and the outro. When the auth module it found exports that provider's own
  instance rather than a `getSession`, it writes
  `server/lib/flowpanel-session.ts` built on the matching preset
  ([`withBetterAuth`](/docs/guides/auth-with-better-auth),
  [`withClerk`](/docs/guides/auth-with-clerk), `withNextAuth`, `withLucia`) and
  points the config at it. A module that already exports `getSession` is left
  alone, and an unrecognised project still gets the closed `getSession` adapter.
  Connect your role mapping either way; async role lookups are supported. Use
  `--dev-auth` only to explicitly opt into an open development-only identity.
- **Stylesheet** — `admin.css` imports `@flowpanel/kit/styles/admin.css`, the
  precompiled and isolated FlowPanel stylesheet. It does not require Tailwind,
  a Tailwind config, source/content directives, or a PostCSS plugin.
- **The install step** uses the package manager it detected — npm, pnpm, yarn
  or bun — and is skipped for packages already in your `package.json`.
- **The next-steps outro** is written in that manager's own vocabulary
  (`pnpm exec flowpanel migrate` vs `npx flowpanel migrate`, and so on).

### When `init` stops

- **No Next.js in `package.json`** — aborts, exit `1`, with the install
  command for your package manager.
- **No Drizzle and no Prisma** — aborts, exit `1`, same treatment.
- **No TTY and no `--yes`, `--dry-run` or `--json`** — `init` asks questions, so a non-interactive run
  without `--yes` exits `1` immediately: *"init asks questions and this run
  has no interactive terminal. Re-run with --yes to accept the detected
  defaults."* Pass `--yes` in CI.
- **A failed dependency install** does not roll back the files. It prints the
  package manager's own last lines, the commands to run by hand, and sets
  exit code `1` so the run is not mistaken for a success.
- **A dependency that installs but cannot be used** is treated the same way. A
  `@flowpanel/kit` from another minor, or a project-local `@flowpanel/cli` from
  another minor than the one you ran, is listed in `dependencies.failed` with
  its own recovery command and exits `1`. A different patch or a matching
  preview of the same minor is accepted.

Re-running `init` is idempotent: matching generated files become `skip`.
If any target exists with different content, the plan reports a conflict and
nothing is written. Merge that file manually, then rerun. `--yes` accepts
detected paths; it does not grant permission to overwrite source.

## flowpanel migrate

**Usage:** `flowpanel migrate [options]`

Apply SQL migrations from flowpanel/migrations/

| Option | Description | Default |
| --- | --- | --- |
| `--dry-run` | Print migrations that would be applied without running them | — |
| `--json` | Emit machine-readable JSON | — |

Apply SQL migrations from `flowpanel/migrations/`. The CLI dynamically
imports your `flowpanel.config.ts` (via `jiti`) so it can reach the
adapter's `db` instance and run the migration there.

```bash
$ pnpm flowpanel migrate
```

Use `--dry-run` to inspect migration files without importing your config or
opening a database connection. Because it deliberately cannot know the applied
state, it lists every SQL file and reports `appliedStateKnown: false` in JSON.
Run the command without `--dry-run` to compare those files with the database.
Add `--json` for a stable pending/applied payload. The CLI tracks applied IDs in
`_flowpanel_migrations`.

Each pending file is handed to the selected migration executor. A modern
adapter provides one `applyMigration(id, sql)` operation; the compatibility
executor composes the legacy `runMigrationSql` and `markMigrationApplied`
hooks. On PostgreSQL and SQLite the modern operation keeps SQL and bookkeeping
in one transaction. MySQL DDL implicitly commits, so review and back up
production migrations.

The applied-ID list is a preflight snapshot, not a process lock. Both shipped
adapters serialize at the database, recheck the ID under that lock and treat a
concurrent duplicate as a no-op. PostgreSQL and SQLite keep the file and its
marker in one transaction. MySQL implicitly commits DDL, so it is serialized
instead: Drizzle pins a connection behind an advisory lock, Prisma claims a
durable row that also records where a failed run stopped.

Custom adapters should implement `applyMigration` and `listAppliedMigrations`.
During the 0.2 upgrade, the CLI still accepts the legacy `runMigrationSql` plus
`markMigrationApplied` pair and warns that it is non-atomic and does not
serialize concurrent migrators; JSON output then exposes
`migrationMode: "legacy-hooks"` and a non-empty `warnings` array, so CI can
reject it explicitly. Both shipped adapters implement the locked operation and
never take that path.

Both adapters reject dialect-specific procedural scripts without PostgreSQL
dollar-quoted bodies before executing them; Prisma also rejects MySQL
executable comments and client directives such as `DELIMITER`. Use the ORM's
native migration workflow for those files.

Every command reads `.env.local` and then `.env` from the working directory
before it runs, the same order Next.js uses, so a `DATABASE_URL` that already
works for `next dev` works here too. Variables already set in the shell win, so
`DATABASE_URL=… pnpm flowpanel migrate` still overrides the file. When the
connection fails and no `DATABASE_URL` is set anywhere, the CLI says so instead
of printing a driver stack trace; `FLOWPANEL_DEBUG=1` brings the stack back.

`jiti` is loaded on demand; when it is missing the command prints the install
line for **your** package manager (`pnpm add -D jiti`, `npm install --save-dev
jiti`, …) and exits `1`.

## flowpanel doctor

**Usage:** `flowpanel doctor [options]`

Check FlowPanel health and wiring

| Option | Description | Default |
| --- | --- | --- |
| `--fix` | Auto-fix missing route files from templates | — |
| `--dry-run` | Show fixes without writing (use with --fix) | — |
| `--json` | Emit machine-readable JSON | — |
| `--path <url>` | Resolved admin URL when the config uses a dynamic path | — |

Run the wiring checks, in this order:

- Next.js ≥ 16.3 and < 17, and TypeScript installed
- an ORM the adapters support — Drizzle or Prisma
- `flowpanel.config.ts` exists
- the four generated files exist: the API route, the SSE route, the seed
  migration, and the catch-all admin page
- only one reachable `@flowpanel/core` installation across the installed
  dependency graph, including package-local peers and third-party plugins.
  Stale pnpm store entries are ignored; uninstalled projects skip this check
- no destructive row/bulk action declared without a `confirm`
- `tsc --noEmit` passes

Mount checks follow URL segments through route groups, parallel slots, and
intercepting routes. Existing pages inside the selected admin branch remain
protected. Sibling dynamic routes such as `[locale]` and parent catch-all fallbacks
can coexist with a static admin branch; Next gives the static branch precedence.
The selected admin URL is therefore reserved for FlowPanel even if a fallback
previously matched it.

```bash
$ pnpm flowpanel doctor
$ pnpm flowpanel doctor --fix
$ pnpm flowpanel doctor --fix --dry-run
```

`--fix` creates whichever of the four generated files are missing from the
same templates `init` uses. The fix is one transaction; `--dry-run` shows it
without writing. The other failures have no auto-fix and print a hint instead.

Exit code is non-zero when any check fails — wire it into CI to catch
drift on every PR.

## flowpanel eject

**Usage:** `flowpanel eject [options] <target> [name]`

Eject a FlowPanel piece into your app/admin folder. Targets: resource <name>, dashboard <path>, layout.

| Argument | Status |
| --- | --- |
| `<target>` | required |
| `[name]` | optional |

| Option | Description | Default |
| --- | --- | --- |
| `--force` | Overwrite files if they already exist | — |
| `--dry-run` | Print the filesystem plan without writing | — |
| `--json` | Emit machine-readable JSON | — |

Eject one of three targets — `resource`, `dashboard`, or `layout` —
into your repo. See [Customization / Eject](/docs/customization/eject).

```bash
$ pnpm flowpanel eject resource users
$ pnpm flowpanel eject dashboard "/monitoring"
$ pnpm flowpanel eject layout
```

It lists every file it wrote, then comments the matching config entry out
(uncomment to revert). Template files and the config edit are committed as one
rollback-safe transaction. Pass `--force` to explicitly replace an existing
eject target; without it, a conflict aborts before any file changes.

## flowpanel dev

**Usage:** `flowpanel dev [options]`

Run Next.js dev server (and bull-board if REDIS_URL is set)

| Option | Description | Default |
| --- | --- | --- |
| `--port <port>` | Next.js dev port | `3000` |
| `--no-board` | Skip the bull-board server even if REDIS_URL is set | — |

Wrapper around `next dev` that also spawns `bull-board` when a
`scripts/board-server.mts` (or `.ts`) file is present and `REDIS_URL` is set.

```bash
$ pnpm flowpanel dev --port 3000
```

Both children are launched through the **detected** package manager's runner —
`pnpm exec next dev`, `npx next dev`, `yarn next dev`, `bunx next dev` — so
the local binaries resolve whichever manager installed them. Output is
prefixed `[next]` / `[board]`, and either child exiting shuts the other down.

Pass `--no-board` to skip the queue dashboard even when Redis is up. With
`REDIS_URL` set but no `scripts/board-server.mts` or `.ts` on disk, the board is skipped
with a one-line note rather than an error.

## flowpanel new

**Usage:** `flowpanel new [options] <resource>`

Add a resource(...) entry to flowpanel.config.ts

| Argument | Status |
| --- | --- |
| `<resource>` | required |

| Option | Description | Default |
| --- | --- | --- |
| `--table <expr>` | First argument expression (default: schema.<resource>) | — |
| `--kind <kind>` | Adapter kind: drizzle (default) or prisma | `drizzle` |
| `--dry-run` | Print the filesystem plan without writing | — |
| `--json` | Emit machine-readable JSON | — |

Append a `resource(...)` entry to your existing
`flowpanel.config.ts` without re-running `init`.

```bash
$ pnpm flowpanel new posts
```

```text
└  Added resource "posts" to flowpanel.config.ts
```

It writes `resource(schema.posts, { columns: ["id"] })` into the `resources`
array — creating the array when `defineAdmin` has none — and **adds `resource`
to the existing `@flowpanel/kit` import** when the config does not already
bind that name, so the file compiles as written. Existing comments in the
array are preserved (the entry is spliced as text, not reprinted).

Before writing, a Drizzle-kind run resolves the config's `import * as schema`
line and checks that the module really exports `posts`. A miss aborts with
exit `1` and lists what the schema does export; an unverifiable case (no
`import * as schema`, an unresolvable specifier) warns and writes anyway.
A resource that already exists aborts with exit `1`.

Flags: `--table <expr>` overrides the first argument (default
`schema.<resource>`, and skips the schema check); `--kind drizzle|prisma`
toggles the template (default `drizzle`; `prisma` writes
`resource<unknown>("posts", …)`).

`--dry-run` shows the config modification without applying it. `--json`
returns the same plan/result envelope used by `init`, `doctor` and `eject`.
