---
title: "Generators (adonia:* ace commands)"
image: "https://adonia.pages.dev/og.png"
version: "next"
---

> Documentation Index
> Fetch the complete documentation index at: https://adonia.pages.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Generators (adonia:* ace commands)

Implements the TECH_SPEC §3 command table. The commands ship in
`@adonia/devtools` and are registered through `adonisrc.ts`:

```ts
import { defineConfig } from '@adonisjs/core/app'

export default defineConfig({
  commands: [() => import('@adonia/devtools/commands')],
})
```

`node ace add @adonia/core` adds that entry for you.

| Command | Writes |
|---|---|
| `adonia:resource <Model>` | `app/adonia/resources/<model>_resource.ts` |
| `adonia:action <name>` | `app/adonia/actions/<name>_action.ts` |
| `adonia:widget <name>` | `app/adonia/widgets/<name>_widget.ts` |
| `adonia:field <name>` | `app/adonia/fields/<name>_field.ts` + `inertia/adonia/fields/<name>_field.tsx` |
| `adonia:page <name>` | `app/adonia/pages/<name>_page.ts` + `inertia/pages/adonia/<name>.tsx` |
| `adonia:eject <page>` | replaces `inertia/pages/adonia/<page>.tsx` |
| `adonia:doctor` | nothing — see [doctor.md](/doctor) |

Every generator refuses to overwrite an existing file without `--force`.

---

## `adonia:resource <Model>`

Reads the model's columns and writes a resource whose schema mirrors the table.

```
node ace adonia:resource Post
node ace adonia:resource Post --fields=title,slug,body --no-table
node ace adonia:resource Post --panel=admin
```

### Where the facts come from

Two sources are merged, plus one probe:

| Fact | Source |
|---|---|
| attributes, column names, primary key, `serializeAs: null` | `Model.$columnsDefinitions` |
| `date` vs `datetime`, `createdAt`/`updatedAt` markers | the column's Lucid `meta` |
| storage type, NOT NULL, declared length | `columnsInfo()` (knex) |
| enum members | a per-dialect probe (below) |

Enum members are not part of `columnInfo` in any dialect, so they are recovered
separately: from the stored `CREATE TABLE` text on SQLite, from
`information_schema.COLUMNS.COLUMN_TYPE` on MySQL, from `pg_get_constraintdef`
plus `pg_enum` on PostgreSQL, and from `sys.check_constraints` on MSSQL. A
dialect without a probe, or a probe that fails, leaves the column as plain text
and prints why.

### Type mapping

| Column type | Field | Column (`table()`) |
|---|---|---|
| `varchar`, `char`, `uuid` | `F.text` | `C.text` |
| `text`, `longtext`, `citext` | `F.textarea` | `C.text` |
| `integer`, `bigint`, `smallint` | `F.number` | `C.number` |
| `decimal`, `numeric`, `float`, `real` | `F.number` | `C.number` |
| `boolean`, `bit` | `F.toggle` | `C.boolean` |
| `date` | `F.date` | `C.date` |
| `datetime`, `timestamp` | `F.datetime` | `C.date` |
| `time` | `F.time` | `C.text` |
| enum | `F.select().options({...})` | `C.badge` |
| `json`, `jsonb` | `F.json` when core exposes it, else `F.textarea` | — |

`NOT NULL` becomes `.required()`, a nullable column `.nullable()`, and a
declared string length `.maxLength(n)`. The generator only emits factories the
installed `@adonia/core` actually exports, so it can never produce a call into
an API that has not shipped.

### What is left out of the form

Three exclusions, all §19-shaped rather than cosmetic:

- **primary keys** — assigned by the database; a fillable `id` is a
  mass-assignment hole;
- **timestamps** — `autoCreate`/`autoUpdate` columns and
  `createdAt`/`updatedAt`/`deletedAt`, which are record bookkeeping (and
  `deletedAt` is a tombstone the soft-delete scopes own);
- **`serializeAs: null` attributes** — password hashes and tokens are declared
  never-to-leave-the-server, and a form round-trips its state to the client.
  Add one back deliberately, with a `dehydrate()` that hashes it.

`--fields` overrides all three: naming a column explicitly is a deliberate act.

### Flags

- `--fields=a,b,c` — generate only these attributes, in this order. Unknown
  names are reported in the file header; naming *only* unknown ones is an error.
- `--no-table` — omit the `table()` override and let §7.5 derive the index from
  the schema instead.
- `--panel=<id>` — check the resource will surface on a panel that exists,
  validated against `.adonisjs/adonia/manifest.json`. A missing manifest (the
  app has never booted the assembler) is a warning, not an error.
- `--force` — overwrite an existing resource.

### When introspection is impossible

An unreachable database, an unmigrated table or a missing Lucid never fail the
command. The generator falls back to what the ORM alone knows — attribute
names, date columns, the primary key, hidden columns — and records the reason
twice: once as a terminal warning and once as a `NOTE:` in the generated file's
header, because the file outlives the scrollback.

---

## `adonia:eject <page>`

Replaces a scaffolded thin re-export under `inertia/pages/adonia/` with the
full packaged page source (TECH_SPEC §13.4).

```
node ace adonia:eject resource_index
node ace adonia:eject error --force
```

The packaged source is read from the installed `@adonia/ui` (its shipped
TypeScript source; the build output is a fallback, reported as a warning), and
every relative import is rewritten onto the `@adonia/ui` package entry and
merged into one value import and one type import. That is the §13.4 promise:
an ejected page keeps receiving component and hook fixes through the package.

Rewriting is checked, not assumed — if a packaged page imported something the
root entry does not re-export, ejection fails with the symbol name rather than
writing a file that will not compile.

A page that no longer re-exports from `@adonia/ui/pages` is treated as yours
and is not overwritten without `--force`.

---

## `adonia:action`, `adonia:widget`, `adonia:field`, `adonia:page`

These four target features whose runtime lands in Phase 3 (the action DSL, the
widget DSL, dashboards, the `BasePage` contract). Their output is written
against what exists **today** and compiles untouched — no imports of APIs that
have not shipped:

- **`adonia:action <name>`** — a plain injectable with `slug`, `label` and
  `handle(ctx, targets)`, the signature the DSL will consume. Callable and
  testable now.
- **`adonia:widget <name> --type=value|trend|partition|table|custom`** — a class
  whose `data()` returns the payload shape that kind's client component reads.
  The return type is written into the file, so a widget cannot quietly return a
  shape the renderer will not understand.
- **`adonia:field <name>`** — a complete, working pair: a real `Field` subclass
  with its §11.1 Vine base, a real `FieldProps` component, and the printed
  `r.field('<type>', Component)` snippet for `inertia/adonia.ts`. Nothing here
  is waiting on Phase 3. `--type` sets the descriptor type string (default
  `app/<dashed-name>`); namespace it, because §13.2 registration is
  last-write-wins.
- **`adonia:page <name>`** — a page class carrying the `slug`/`component`/`title`
  statics `GET <panel>/pages/:page` already resolves, plus an Inertia component
  wrapped in `PanelShell`. Register it with
  `Panel.make('admin').pages([() => import('#adonia/pages/<name>_page')])` and
  it is reachable immediately.

---

## Naming conventions

`Post` → slug `posts`, file `post_resource.ts`, labels `Post`/`Posts`.
Pluralization is conventional (`-y → -ies`, sibilant `-es`, a short irregular
list); the generated `static slug` is one editable line when it guesses wrong.

Source: https://adonia.pages.dev/generators/index.mdx
