CLI reference¶
Every command Nebula adds to bin/magento, grouped by what you would be doing when you
reach for it. All thirteen live under the nebula: namespace; one stock Magento command is
documented at the end because it sits next to the passkey reset.
| Command | One line |
|---|---|
nebula:compile |
Compile every definition into one manifest. Required in production. |
nebula:lint |
Validate every definition JSON against its schema. |
nebula:lint:options |
Check that every option source a definition names can answer. |
nebula:doctor |
Health report across the installed Nebula modules. |
nebula:generate:grid |
Scaffold a grid definition and its layout XML. |
nebula:generate:form |
Scaffold a form definition and its layout XML. |
nebula:crud:generate |
Scaffold a whole admin CRUD module from a database table. |
nebula:generate:schema-docs |
Render the JSON schemas as Markdown. |
nebula:ui:transform |
Draft Nebula JSON from Magento UI component XML. |
nebula:media:index |
Build the media listing index. |
nebula:catalog:fix-category-tree |
Detect and repair category tree damage. |
nebula:passkey:register |
Show an admin where to register a passkey. |
nebula:passkey:reset-two-step |
Turn an admin's second factor off from the shell. |
Definitions¶
nebula:compile¶
Compile every Nebula definition into generated/nebula/definitions.php.
It scans view/adminhtml/{grid,form,snippet,component}/*.json across every
enabled module, resolves each one fully (loader, merger and snippet resolver, exactly as a request
would), validates the merged payload against its schema, and writes a deterministic PHP
manifest keyed by "<type>:<id>".
| Option | Value | Help text |
|---|---|---|
--verify |
flag | Compile and compare against the existing manifest; fail if drift detected. |
--only |
required | Compile a single definition by "type:id" (e.g. grid:product_listing). |
An explicit step on every production deploy
The definition resolver has three modes. In developer mode it resolves live and never
caches. In default mode it resolves live and caches through nebula_definitions. In
production mode it reads the compiled manifest — and when the manifest is missing or a
definition is not in it, it no longer throws: it logs one warning per definition and
serves the cached live path, so a wiped generated/ degrades to default-mode behaviour
instead of a 500 on the first Nebula page. That fallback is a safety net, not a mode. Run
this command after setup:upgrade on every production deploy.
setup:di:compile does not run it for you. Earlier releases shipped a plugin on that
command; it could never fire, because setup commands are built by the Laminas service
manager rather than the object manager, and it has been removed rather than left to
promise something it did not do.
Output on success:
--verify is the CI form. It compiles in memory and compares, writing nothing:
and on drift:
Post-merge schema problems are reported as non-blocking warnings, because the authoritative
check on raw JSON is nebula:lint. Resolution errors are fatal and the manifest is not
written.
Source: code/Nebula/Component/Console/Command/CompileCommand.php, code/Nebula/Component/Model/Definition/Resolver.php.
nebula:lint¶
Validate every Nebula JSON definition against its Draft 2020-12 schema.
| Option | Value | Help text |
|---|---|---|
--fix |
flag | Apply automated fixes (reformat, alphabetize, auto-position). |
--no-strict |
flag | Relax validation: skip additionalProperties checks. |
--path |
required | Validate a single file or directory instead of scanning all enabled modules. |
Beyond schema conformance it also holds each file to two conventions the schema cannot
express: the id must match the filename stem, and references must resolve.
--fix rewrites files in place — alphabetised columns, auto-assigned position values,
two-space indent, trailing newline — and reports how many it touched.
This is the command to put in CI. It exits non-zero on the first file with errors and prints each error under the filename it came from:
view/adminhtml/grid/acme_thing_listing.json
[#/columns/status] value "toggl" is not one of the allowed types
1 file(s) with errors.
Source: code/Nebula/Component/Console/Command/LintCommand.php.
nebula:lint:options¶
Check that every optionsSource / filterOptionsSource in every definition resolves, and
that its optionsMethod exists. No arguments, no options.
nebula:lint proves a definition matches its schema, and the schema says optionsSource is
a string — not that the alias is registered, and not that optionsMethod names a method the
resolved class actually has. Both failures are silent at runtime: the option resolver catches
everything and returns [], so a missing alias, a misspelled method and an unreadable return
shape all render as a select with no options. On a required field that is a form which cannot
be submitted, on a page that looks entirely normal.
It deliberately does not flag an empty option list. Emptiness is a fact about data and request context, not about the definition — a source that distinguishes over the current admin's own rows is correctly empty in a CLI run and correctly populated in the browser. The command asserts only what a definition can be held to: the alias resolves, and the method is there to call.
Run it in CI beside nebula:lint, and any time you add or rename an option source.
Source: code/Nebula/Component/Console/Command/LintOptionSourcesCommand.php.
nebula:doctor¶
Report health of the Nebula framework (modules, definitions, cache, renderers). No arguments, no options. This is the command to paste into a support ticket.
Nebula Doctor — sprint health report
--------------------------------------------------------
Nebula modules 24
JSON definitions (total) 220
- grid 83
- form 55
- snippet 71
Cache type "nebula_definitions" enabled
Unresolved @ref in definitions 0
Renderer pool (fields / columns) 1 / 10
ObjectManager usages (non-test) 0 (target: <=5)
Grid collections (disallowed FQCN) 0 (target: 0)
Storefront companion packages OK
Registries declared in an area di.xml OK
--------------------------------------------------------
Doctor: OK
What each row counts, and what a healthy install reads:
- Nebula modules counts enabled modules whose name starts with
Nebula_Core. The metapackage installs 26 modules, two of which sit outside that prefix, so 24 is the healthy reading on a complete install. It is not two missing modules. - JSON definitions counts the
grid,formandsnippetJSON files in those same modules. The figures move with every release, so read the block as a shape rather than a baseline, and do not expect the total to matchnebula:compile, which scans every enabled module and countscomponentdefinitions as well. - Cache type reads
enabledon a healthy install.disabledmatters only outside developer mode, since the developer-mode resolver never caches anyway.not configuredmeans the cache type is not in the merged configuration at all. It cannot mean the module is missing, because this command ships inNebula_Componentitself, so suspect a stale config cache. - Renderer pool is the number of field renderers and column renderers registered on
RendererPool. Every renderer a module contributes raises it, so the healthy value is whatever your install has contributed, never zero. - ObjectManager usages (non-test) is a source-tree metric. It scans
app/code/Nebulaonly, so it reads0on any install whose modules live invendor/, which covers both Composer routes in Install. A non-zero value means a repository checkout or a copy intoapp/code. Its target is advisory: a value above it still exits0. Every other row resolves module directories and is correct wherever the modules are installed. - Storefront companion packages reads
OKunless a finding applies, and a finding is printed as prose rather than a number. - Registries declared in an area
di.xmlis red, and spelled out per module, when any enabled module declares one of the eight engine registries (RendererPool, the data provider, option source, filter option source, save handler and reference resolver registries,CollectionRegistry,Translationsproviders) underetc/adminhtml/di.xmlor another area file. Magento merges one area's files deeply, but an area file's argument replaces the global declaration for that area, so one such block discards every other module's registrations — including Nebula's own. Move the block toetc/di.xml. See Extension points.
Two metrics decide the exit code: unresolved @ref and grid collections written as a
raw class name. Either one non-zero exits 1 with Doctor: issues detected, because both
mean a definition is structurally broken.
The storefront-companion check is deliberately outside the exit code. It reports a storefront
feature that is active but whose companion package is not installed — a condition that fails
silently with the page still answering 200 — and it spells the finding out in prose rather
than leaving it as a number, ending in a runnable composer require. An install that
legitimately runs without the companion should not start failing a gate it never agreed to.
The pairings are data, not code. A theme stack declares its own by adding an item to the
pairings array on Nebula\Component\Model\Diagnostics\StorefrontPairingCheck in
etc/di.xml, with four keys. theme is the theme path, such as Vendor/ThemeName.
requires is the companion module that must be installed. when maps a feature label to the
module whose presence arms the check, and any one of them being enabled is enough. because
is the prose the report prints. A finding needs all three of an absent companion, at least one
armed module enabled, and the theme active on at least one store view, so an install that does
not use the theme hears nothing. Every value is a plain string, and nothing there is
type-hinted or autoloaded, so naming a package an install does not have costs nothing.
Source: code/Nebula/Component/Console/Command/DoctorCommand.php.
Scaffolding¶
nebula:generate:grid¶
Generate a skeleton Nebula grid definition and layout XML.
| Argument | Required | Help text |
|---|---|---|
module |
yes | Module name, e.g. Vendor_Module. |
id |
yes | Grid identifier, e.g. my_listing. |
$ bin/magento nebula:generate:grid Acme_Blog acme_blog_post_listing
Created: /…/Acme/Blog/view/adminhtml/grid/acme_blog_post_listing.json
Created: /…/Acme/Blog/view/adminhtml/layout/acme_blog_post_index.xml
An unregistered module name fails before anything is written.
Source: code/Nebula/Component/Console/Command/GenerateGridCommand.php.
nebula:generate:form¶
Generate a skeleton Nebula form definition and layout XML. Same shape as the grid generator.
| Argument | Required | Help text |
|---|---|---|
module |
yes | Module name, e.g. Vendor_Module. |
id |
yes | Form identifier, e.g. my_entity_edit. |
Source: code/Nebula/Component/Console/Command/GenerateFormCommand.php.
nebula:crud:generate¶
Generate a full Nebula admin CRUD module from a database table — model, resource model, collection, repository and data interfaces, controllers, grid and form definitions, layout, menu and ACL. Omit the table argument to enter interactive mode.
| Argument | Required | Help text |
|---|---|---|
table |
no | Declared table name, e.g. qoliber_blog_post. Omit to enter interactive mode. |
| Option | Value | Help text |
|---|---|---|
--module |
required | Target module, e.g. Vendor_Module. |
--schema |
required | Explicit db_schema.xml path. |
--entity |
required | Entity base name. |
--label |
required | Human label column. |
--exclude |
required | Columns to omit (comma separated). |
--hidden |
required | Columns rendered hidden. |
--readonly |
required | Columns rendered read only. |
--select |
required | Force select col:Source (repeatable, comma separated). |
--wysiwyg |
required | Force wysiwyg columns. |
--searchable |
required | Grid searchable columns. |
--grid-cols |
required | Grid column set. |
--menu-parent |
required | Parent menu id. |
--menu-title |
required | Menu label. |
--sort-order |
required | Menu sort order. |
--acl |
required | ACL resource id. |
--route |
required | Admin route front name. |
--dry-run |
flag | Run the pipeline, write nothing. |
--force |
flag | Overwrite existing files. |
--compile |
flag | Run setup:di:compile after writing. |
--select takes a registered alias, not a class name
The value is column:alias, and the alias must be one registered on
OptionSourceRegistry: lowercase, dot separated, as in
--select=status:acme.blog.post.status. The form schema rejects a class name outright,
and because every generated file is validated before anything is written, that rejection
aborts the whole run and nothing is written. An entry with no colon fails earlier, with
Malformed --select entry. Register the alias in your module's etc/di.xml first. A
grid's filterOptionsSource follows the same rule.
bin/magento nebula:crud:generate acme_blog_post \
--module=Acme_Blog --entity=Post --label=title \
--menu-parent=Magento_Backend::content --acl=Acme_Blog::post \
--dry-run
Start with --dry-run: it runs the full pipeline including validation and reports what it
would write, without touching the filesystem.
What it derives. The ids, route, ACL and menu placement you do not pass are derived from the table name and the module name.
| Derived | Value |
|---|---|
| Entity name | the table name with any leading vendor and module segments stripped, StudlyCased, then singularized |
| Grid id | <table>_listing |
| Form id | <table>_edit |
| ACL resource | <Vendor_Module>::<entity lowercased>, so Acme_Blog and Post give Acme_Blog::post |
| Route front name | vendor_module, both halves lowercased |
| Menu parent | Magento_Backend::content |
| Menu title | the entity label, pluralized |
| Sort order | 10 |
| Default sort | created_at desc when the table has that column, otherwise the primary key ascending |
It also writes two DI bindings you will need when you extend the module by hand:
<table>.collection on Nebula\Grid\Model\Registry\CollectionRegistry, and
<table>.form on Nebula\Component\Model\Registry\DataProviderRegistry. The generated
grid does not name a collection class; it uses the built-in grid.collection provider pointed
at the collection alias.
Column and field labels come from the comment attribute on the column in db_schema.xml,
falling back to a title-cased column name. Writing comments there is how you control the
generated labels without editing the JSON afterwards.
Tables the generator refuses
The table must have exactly one primary-key column. A table with no primary key is
rejected, and so is a composite one, with an error naming the columns it found. A typo in
a column-targeting flag, meaning --exclude, --hidden, --readonly, --wysiwyg,
--select, --searchable or --grid-cols, aborts the run with Unknown column(s)
referenced by flags and prints the table's real column list, rather than ignoring the
flag in silence.
The table definition is folded across every module that declares it, as
Generators describes, and the fold honours a later module's
disabled="true" removal of a column. --schema bypasses the fold and reads the one file you
name.
Source: code/Nebula/CrudGenerator/Console/Command/GenerateCrudCommand.php.
nebula:generate:schema-docs¶
Render the Nebula JSON schemas as Markdown reference docs.
| Option | Value | Help text |
|---|---|---|
--check |
flag | Exit non-zero if generated docs would differ from committed docs (CI drift check). |
Output is deterministic: re-running on unchanged schemas produces byte-identical files, which
is what makes --check safe to wire into CI.
$ bin/magento nebula:generate:schema-docs
wrote /…/docs/schema/grid.md
wrote /…/docs/schema/form.md
$ bin/magento nebula:generate:schema-docs --check
Schema docs are up to date.
On drift it names each stale file and exits non-zero.
Source: code/Nebula/Component/Console/Command/GenerateSchemaDocsCommand.php.
nebula:ui:transform¶
Generate draft Nebula JSON and layout stubs from Magento UI component XML. This is the offline route: it reads XML from disk and writes drafts, with no runtime interception.
| Argument | Required | Help text |
|---|---|---|
path |
yes | Module path, ui_component directory, or single ui_component XML file. |
| Option | Value | Help text |
|---|---|---|
--output-dir |
required | Optional output directory. Defaults to var/nebula-ui-transformer/<source-name>. |
$ bin/magento nebula:ui:transform vendor/acme/module-blog
Source: /…/vendor/acme/module-blog
Output: /…/var/nebula-ui-transformer/module-blog
Components: 2
acme_blog_post_listing (listing)
JSON: /…/acme_blog_post_listing.json
Layout: /…/acme_blog_post_index.xml
acme_blog_post_form (form)
JSON: /…/acme_blog_post_form.json
Warning: Potential EAV form detected.
Done.
The drafts are a starting point to hand-finish and commit. For the runtime alternative — a third-party component rendered as it stands, without a committed definition — see the Nebula UI Bridge, and Generators for the wider scaffolding story.
Source: code/Nebula/UiTransformer/Console/Command/TransformUiComponentCommand.php.
Data and maintenance¶
nebula:media:index¶
Build the Nebula media listing index.
| Argument | Required | Help text |
|---|---|---|
path |
no | Media-relative subtree to index, e.g. "wysiwyg". Defaults to the whole media root. |
| Option | Short | Value | Help text |
|---|---|---|---|
--force |
-f |
flag | Walk every directory even if its mtime has not moved, and re-read every file. |
The media page reads this index instead of walking the filesystem, so a library that has never been indexed shows an empty page with a "not indexed" notice rather than a wrong one. Running this is part of installing the feature, not an optimisation to apply later.
It is safe to run repeatedly and safe to interrupt. A directory whose mtime has not moved since the last run is skipped without being read, and each directory is written before the next is read, so an interrupted run leaves the finished directories correct and the rest merely stale — re-run to continue.
--force exists because a directory's mtime does not change when a file inside it is edited
in place. The per-file mtime check still catches that on any directory that is walked;
--force is what makes every directory get walked.
$ bin/magento nebula:media:index
Indexing the whole media root
Done in 4.2s. 118 directories walked, 0 unchanged and skipped.
2,930 file(s) seen, 2,930 row(s) written, 0 row(s) removed.
Index now holds 2,614 browsable asset(s) across 96 directories.
Add -v for a line per directory. If the index ends up holding zero browsable assets the
command says why: only folders listed under
system/media_storage_configuration/allowed_resources/media_gallery_image_folders are
listed, and everything else is indexed but hidden.
Source: code/Nebula/Media/Console/Command/IndexMediaCommand.php.
nebula:catalog:fix-category-tree¶
Detect and repair category parent_id / path / level inconsistencies.
| Option | Value | Help text |
|---|---|---|
--dry-run |
flag | Report inconsistent categories without changing anything. |
Direction of repair is not the operator's guess. For each damaged row the tree invariant decides which two of the three columns corroborate each other and repairs the third accordingly, so both kinds of damage are fixed in one pass and neither can undo the other.
A real repair rebuilds two indexers in full
After writing its repairs the command runs reindexAll() on catalog_category_product
and catalog_product_category. That is a full rebuild of both indexers, synchronously,
inside the command, not a delta. On a large catalog it is minutes of database load, so run
the repair in a maintenance window or off peak. --dry-run never reindexes. If one of the
two reindexes fails the command names it and carries on, so the repair itself is not lost.
$ bin/magento nebula:catalog:fix-category-tree --dry-run
…
Dry run: no changes made. Re-run without --dry-run to repair.
A clean tree reports Category tree is consistent. Nothing to repair. and exits 0. After a
real repair, flush the cache to refresh the storefront. Rows the command cannot repair safely
are reported as blocked and make it exit non-zero.
Source: code/Nebula/Grid/Console/Command/FixCategoryTree.php.
Admin access¶
nebula:passkey:register¶
Show an admin user where to register a passkey, and how many they have.
| Option | Short | Value | Help text |
|---|---|---|---|
--user |
-u |
required | Admin username. |
It does not mint a registration challenge, and cannot. A WebAuthn ceremony has to be started by the browser that will finish it, on the origin it is scoped to; a challenge issued by a CLI process minutes earlier is not something a page can adopt. So the command's honest job is to count what exists and point at the browser flow.
$ bin/magento nebula:passkey:register --user=jane
Passkeys for admin user "jane": 0 registered.
A passkey is created by the browser that will use it, so registration
cannot be started from the command line. Sign in with a password, then:
System > My Passkeys, or go straight to
https://example.com/admin/nebulapasskey/passkey/index
and press Add Passkey.
Source: code/Nebula/Passkey/Console/Command/RegisterPasskeyCommand.php.
nebula:passkey:reset-two-step¶
Turn off an admin user's two-step verification, so they can sign in again.
| Option | Short | Value | Help text |
|---|---|---|---|
--user |
-u |
required | Admin username. |
This is the way back in when an administrator's Nebula authenticator and recovery codes are both gone. A customer can be reset by a merchant from the admin; an administrator has no such backstop, because the person who could help them is behind the same gate.
There is no confirmation prompt, deliberately. Reaching the shell on the server is already a higher privilege than any admin session, so a prompt would guard nothing from anybody able to run the command, and it would make the command useless in the situation it exists for — a deploy script, or a colleague on a phone reading it out.
It resets one administrator's enrolment, not the requirement. There is no install-wide off switch here: "two-step verification is off for everyone" is a configuration decision made deliberately, not a side effect of helping one person back in.
What it removes is narrow, so read it before reaching for it in a lockout. It clears that
administrator's Nebula TOTP secret and their remaining recovery codes, and forgets the
two-step session. It does not touch their registered passkeys, and it does not touch
Magento_TwoFactorAuth's own per-user provider configuration. When the core two-factor
module is enabled, which it is on a stock install, the command that unblocks the account is
security:tfa:reset instead.
$ bin/magento nebula:passkey:reset-two-step --user=jane
Two-step verification is off for jane.
They can sign in with their password, and set it up again from
System > Permissions > Two-step Verification.
An account with nothing set up reports so and exits 0; an unknown username exits 1.
security:tfa:reset (stock Magento)¶
Magento's own reset for Magento_TwoFactorAuth. It clears one user's configuration for one
named provider (google, duo_security, authy, u2f_key, and any provider a module
registers).
Use it when the install is on core two-factor providers, which on a stock install is every
administrator. nebula:passkey:reset-two-step resets something narrower and is not a
substitute: it clears the Nebula TOTP secret, the remaining recovery codes and the two-step
session, and nothing else. Registered passkeys survive it, and the per-user provider
configuration Magento_TwoFactorAuth holds is untouched.
Nebula's own two-step enforcement stands down whenever Magento_TwoFactorAuth is enabled, as
Security describes, so on a stock install the Nebula command usually reports
that the account has no two-step verification set up. The command that unblocks such an
account is security:tfa:reset <user> nebula_passkey, or security:tfa:reset <user> google
if the administrator enrolled the authenticator app instead.
Source: Magento_TwoFactorAuth; the provider code Nebula registers is declared in code/Nebula/Passkey/etc/di.xml.