Implements the TECH_SPEC §3 command table. The commands ship in
@adonia/devtools and are registered through adonisrc.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 |
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=adminWhere 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
idis a mass-assignment hole; - timestamps —
autoCreate/autoUpdatecolumns andcreatedAt/updatedAt/deletedAt, which are record bookkeeping (anddeletedAtis a tombstone the soft-delete scopes own); serializeAs: nullattributes — 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 adehydrate()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 thetable()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 --forceThe 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 withslug,labelandhandle(ctx, targets), the signature the DSL will consume. Callable and testable now.adonia:widget <name> --type=value|trend|partition|table|custom— a class whosedata()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 realFieldsubclass with its §11.1 Vine base, a realFieldPropscomponent, and the printedr.field('<type>', Component)snippet forinertia/adonia.ts. Nothing here is waiting on Phase 3.--typesets the descriptor type string (defaultapp/<dashed-name>); namespace it, because §13.2 registration is last-write-wins.adonia:page <name>— a page class carrying theslug/component/titlestaticsGET <panel>/pages/:pagealready resolves, plus an Inertia component wrapped inPanelShell. Register it withPanel.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.