Skip to content

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

bin/magento nebula:compile [--verify] [--only=TYPE:ID]

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:

Wrote /path/to/generated/nebula/definitions.php (207 definition(s), 207 validated against schemas).

--verify is the CI form. It compiles in memory and compares, writing nothing:

Verify OK: 207 definition(s), 207 validated against schemas.

and on drift:

Manifest drift detected. Re-run "bin/magento nebula:compile" and commit the result.

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

bin/magento nebula:lint [--fix] [--no-strict] [--path=PATH]

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

bin/magento 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

bin/magento 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, form and snippet JSON 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 match nebula:compile, which scans every enabled module and counts component definitions as well.
  • Cache type reads enabled on a healthy install. disabled matters only outside developer mode, since the developer-mode resolver never caches anyway. not configured means the cache type is not in the merged configuration at all. It cannot mean the module is missing, because this command ships in Nebula_Component itself, 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/Nebula only, so it reads 0 on any install whose modules live in vendor/, which covers both Composer routes in Install. A non-zero value means a repository checkout or a copy into app/code. Its target is advisory: a value above it still exits 0. Every other row resolves module directories and is correct wherever the modules are installed.
  • Storefront companion packages reads OK unless a finding applies, and a finding is printed as prose rather than a number.
  • Registries declared in an area di.xml is 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, Translations providers) under etc/adminhtml/di.xml or 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 to etc/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

bin/magento nebula:generate:grid <module> <id>

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

bin/magento nebula:generate:form <module> <id>

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

bin/magento nebula:crud:generate [<table>] [options]

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

bin/magento nebula:generate:schema-docs [--check]

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

bin/magento nebula:ui:transform <path> [--output-dir=DIR]

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

bin/magento nebula:media:index [<path>] [-f|--force]

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

bin/magento nebula:catalog:fix-category-tree [--dry-run]

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

bin/magento nebula:passkey:register --user=USERNAME

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

bin/magento nebula:passkey:reset-two-step --user=USERNAME

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)

bin/magento security:tfa:reset <user> <provider>

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.