navree

Console commands and the CLI

The navree CLI (new, every make:* generator, dev), your app's own commands (serve, migrate, queue:work, ...), and writing your own.

On this page

Navree has two command lines, much like Laravel's laravel installer and Artisan:

  • The navree CLI creates apps, generates code and runs the development loop. It doesn't depend on any Navree crate: everything it knows about the framework is in the code it writes.
  • Your app's binary owns the runtime commands: serve, migrate, queue:work and the rest, registered by the modules you use, plus your own. navree <command> forwards to it, so you rarely call the binary yourself.

Installing the CLI

The CLI is the navree-cli crate. Navree isn't published on crates.io yet, so install it from the Git repository:

Shell

From a clone of the repository, cargo install --path crates/navree-cli does the same.

Shell
Text

Creating an app

navree new <name> creates a working app in ./<name>:

Shell
Text
navree new blog
  • .enva copy of .env.example (never commit it)
  • .env.example
  • Cargo.toml
  • README.md
  • config/
  • app.tomlhttp, database, log, mail, queue, auth, api
  • src/
  • main.rsblog::app(ConfigSources::defaults()).run()
  • lib.rs
  • app.rsthe root: every module, configured once
  • abilities.rswho may do what
  • modules/
    • mod.rs
    • users/profiles at /api/v1/profile, on top of AuthModule
  • tests/
  • app.rs
  • storage/
  • .gitignore
OptionWhat it does
--ui nextAlso creates web/, a Next.js front end that talks to the API through its own server and keeps the tokens in httpOnly cookies, like the CRM's. navree dev starts it. Default: --ui none, API only.
--navree-path <path>Depend on a local checkout (the repository or its crates/navree). Without it, the app gets a path dependency on the checkout the CLI was built from when it still exists (after cargo install --git, Cargo's own copy of the repository), else navree = "0.1.0", which won't build until the crate is on crates.io.

The name may use lower-case letters, digits, - and _. navree new refuses a directory that exists and isn't empty, and formats the generated code with rustfmt when it is installed. The app is its own Cargo workspace; remove the [workspace] table from its Cargo.toml to make it a member of an enclosing one. See Your first app for a walk through it.

Generators

Run generators anywhere inside your app: the CLI finds the package from the current directory. They never overwrite a file (refusing to overwrite stops them before anything is written), and they register what they create by editing your code only at marker comments:

MarkerInWhere generators add
// navree:modulessrc/app.rs.module(...) lines for new modules
// navree:wirea module's wire methodregistrations: controllers, providers, jobs, listeners, templates, commands, migrations
// navree:policiessrc/abilities.rscalls to new policies

When a marker is missing (in code you wrote by hand), the generator prints the line to add as a next hint instead. Names are snake_case; anything else fails with a suggestion (blog_posts for BlogPosts).

make:module

navree make:module <name> generates a feature module and adds it to the app:

Shell
Text

With --crud, the module serves the five JSON resource routes under /api/v1/posts (list, show, create, update, delete), each guarded by abilities on its entity, with validated DTOs and tests. Without it, the module has one authenticated index route to start from. Use the plural for the name: posts gets the entity Post and the table posts. --path changes the modules directory (default src/modules).

Other generators

Every other generator adds one item to an existing module: navree make:<kind> <module> <name> [--path src/modules].

Shell
Text

The report lists a file once per edit: here mod.rs gets the pub mod jobs; declaration and the job's registration.

CommandCreates (in the module)Registers
make:controller posts admincontrollers/admin.rs, AdminControllerthe controller, in wire
make:service posts pricingservices/pricing.rs, PricingServicem.provider::<..>(), in wire
make:entity posts tagentities/tag.rs, a SeaORM entity for tagsnothing; prints the make:migration to run next
make:migration posts create_tags_tablemigrations/m<timestamp>_create_tags_table.rsadds it to the module's migration set. A create_<table>_table name generates a table-creating migration
make:job posts publish_postjobs/publish_post.rs, a job named posts.publish_postthe job, in wire
make:event posts post_publishedevents/post_published.rsnothing (events need no registration)
make:listener posts notify_followers --event post_publishedlisteners/notify_followers.rslisten::<PostPublished, NotifyFollowers>, in wire. Without --event, prints the line to add
make:mail posts welcomemail/welcome.rs with welcome.html and welcome.txtthe templates, in wire
make:notification posts post_likednotifications/post_liked.rs, a database and broadcast notificationnothing
make:policy posts moderationpolicies/moderation.rs, ability rulesa call in src/abilities.rs
make:command posts publish_scheduledcommands/publish_scheduled.rs, the command posts:publish-scheduledm.command(..), in wire
make:test posts publishingtests/publishing.rs, a test booting the moduledeclares it in tests/mod.rs

make:listener --event needs the event to exist (make:event first). The guides for each kind say what to fill in: controllers, entities, migrations, jobs, events and listeners, mail, notifications, policies and tests.

The development server

navree dev builds the app, runs its serve command, and rebuilds and restarts it when the code changes:

Shell
OptionDefaultWhat it does
--debounce-ms <ms>300How long the files must be quiet before a rebuild.
--no-webDon't start the front end in web/.
-- <args>Arguments for the app's serve command.
  • It watches src/, config/ and migrations/, and Cargo.toml, .env and build.rs at the root. Template files under resources/ aren't watched: views reload on their own.
  • It runs a copy of the binary, so the next build can replace the original while the current server keeps answering, and restarts the server only once the new build succeeds. When a build fails, the previous one keeps serving and the compiler errors are shown.
  • With a web/package.json, it also starts the front end's dev server once, with pnpm, yarn or npm depending on the lockfile.

Two limits: the listening socket isn't kept across restarts, so requests fail for the moment the new server takes to start (and open WebSockets drop and reconnect), and if navree dev itself is killed forcibly, the server it started keeps running.

Running your app's commands

Any command the CLI doesn't know is forwarded to your app: navree migrate:fresh --seed runs cargo run --quiet -- migrate:fresh --seed in the current directory and exits with the command's exit code. You can also run the binary directly, which is what you do in production:

Shell

Your app's main hands the command line to AppBuilder::run:

src/main.rs
  • With no command, it runs serve when a module registers it (HttpModule does), otherwise it prints the help.
  • help (also list, --help and -h) lists every command, and check [--graph] validates the module graph; neither boots the app. Through the CLI, use navree list: navree help and navree --help print the CLI's own help.
  • Every other command boots the whole app (configuration, database, every module), runs, then shuts the app down gracefully. A command therefore needs a working configuration and database, even key:generate.
  • An unknown command fails before the app boots. A failing command prints its error and exits with status 1. The error is printed in its Debug form, because main returns it (Error: UnknownCommand { name: "x" }). Ctrl-C (and SIGTERM on Unix) stops long-running commands such as serve and queue:work gracefully.

This is list in the reference CRM, which uses most modules:

Text

The built-in commands

check validates every import, export and provider dependency without constructing anything, which makes it a cheap CI step:

Shell
Text

check --graph prints the module tree instead: each module with its providers, their dependencies, its exports and its commands.

Commands by module

Each module registers its own commands, so list shows only those of the modules your app uses.

CommandModuleExample
serve [--addr HOST:PORT]HttpModulenavree serve --addr 0.0.0.0:8080. Serves until stopped, then drains requests for http.shutdown_grace_secs (30 s).
routes [--json]HttpModulenavree routes prints every route with its name, access rule and module.
down [--retry SECS] [--message TEXT], upHttpModuleSee Maintenance mode.
key:generateCryptoModulePrints a new base64:... key for APP_KEY. See Encryption and hashing.
migrate, migrate:rollback, migrate:statusDatabaseModuleSee Migrations.
migrate:fresh [--seed]DatabaseModuleDrops every table, then migrates (and seeds).
db:seed [name...]DatabaseModuleRuns every seeder, or only the named ones. See Seeding.
cache:clearCacheModuleSee Cache.
queue:work [--queue a,b] [--once] [--stop-when-empty] [--max-jobs N]QueueModuleSee Queues.
queue:failed, queue:retry <id or all>, queue:flushQueueModuleInspect, retry or delete failed jobs.
schedule:run, schedule:work, schedule:listScheduleModuleSee Task scheduling.
access:role, access:grant, access:explainAccessModuleSee Authorization.
audit:prune [--days N]AuditModuleSee The audit log.
openapi:export [path]ApiModuleSee OpenAPI.

A few of them in the CRM, against a freshly seeded database:

Shell
Text
Shell
Text
Shell
Text

Writing commands

A command is a CommandSpec: a name, a one-line description for list, and an async function that gets the booted app's AppContext and the arguments after the command name. navree make:command <module> <name> generates one; here is a complete example:

src/modules/exports/commands/clear.rs

Register it in the module's wire method with m.command(..):

src/modules/exports/mod.rs
Shell
Text
  • Names are unique across the app; two modules registering the same name fail at boot (and in check). Prefix them with the module (exports:clear, crm:task-reminders), like Navree's own queue:work.
  • Arguments arrive as plain strings, with no parsing: read flags and values from args yourself, and return an error for anything you don't understand, as above.
  • Services come from the context: ctx.get::<T>().await? resolves any provider, ignoring module visibility.
  • Output goes to standard output with println!; logs go through tracing as usual.
  • Failure: return Err(..) and the binary prints it and exits with status 1.

Scheduling a command

A command can run on a schedule, as the CRM's daily reminders do (see Task scheduling):

Rust

Testing commands

TestApp::run_command runs a command against the test app, without booting a second one:

src/modules/exports/tests/mod.rs

Popular pages

  • Installation
  • Your first app
  • Routing and controllers
  • Validation
  • Authentication
  • Benchmarks