Runtime Commands
Start, serve, and manage your application's runtime processes — API server, queue workers, scheduler, UI, and developer tools.
npm run maker <command> [options]pnpm maker <command> [options]yarn maker <command> [options]bun maker <command> [options]Development Server
dev
Start the full development stack — API server, UI, queue worker (if Redis enabled), and optional dev tools — all in parallel with a single command.
What starts:
| Process | Started when | Command |
|---|---|---|
| API server | Always | serve --src (hot-reload via tsx watch) |
| UI | UI != "false" | Vite dev server on port 5173 |
| Queue worker | REDIS != "false" | queue:work --queue=default,mail --src |
| Optional tools | --view / --with flags | MailDev, Redis Commander, Drizzle Studio |
All child processes are tracked. If any required process exits unexpectedly, the entire stack shuts down.
npm run maker devpnpm maker devyarn maker devbun maker devAdditional tools
npm run maker dev -- --view=redis,maildev,studio
npm run maker dev -- --with-redis-view --with-maildev --with-db-studiopnpm maker dev --view=redis,maildev,studio
pnpm maker dev --with-redis-view --with-maildev --with-db-studioyarn maker dev --view=redis,maildev,studio
yarn maker dev --with-redis-view --with-maildev --with-db-studiobun maker dev --view=redis,maildev,studio
bun maker dev --with-redis-view --with-maildev --with-db-studio| Tool | Flag | What it provides |
|---|---|---|
| Redis Commander | redis / --with-redis-view | Web UI at port 1369 to inspect Redis keys |
| MailDev | maildev / --with-maildev | SMTP server at port 1089 + web UI at port 1080 to view sent emails |
| Drizzle Studio | studio / --with-db-studio | Web UI at https://local.drizzle.studio for database browsing |
ui:dev
Start only the UI dev server (alias: admin:dev). Useful when you want to run the API separately.
npm run maker ui:devpnpm maker ui:devyarn maker ui:devbun maker ui:devProduction Server
serve
Start the HTTP API server.
Flags:
| Flag | Default | Description |
|---|---|---|
--prod | — | Run compiled dist/src/framework/server.js |
--runtime <name> | node | Runtime: node or bun |
--watch | — | Watch for file changes (source mode) |
--src | — | Force source mode even if dist/ exists |
npm run maker serve
npm run maker serve -- --prod
npm run maker serve -- --prod --runtime=node
npm run maker serve -- --runtime=bun
npm run maker serve -- --watch
npm run maker serve -- --srcpnpm maker serve
pnpm maker serve --prod
pnpm maker serve --prod --runtime=node
pnpm maker serve --runtime=bun
pnpm maker serve --watch
pnpm maker serve --srcyarn maker serve
yarn maker serve --prod
yarn maker serve --prod --runtime=node
yarn maker serve --runtime=bun
yarn maker serve --watch
yarn maker serve --srcbun maker serve
bun maker serve --prod
bun maker serve --prod --runtime=node
bun maker serve --runtime=bun
bun maker serve --watch
bun maker serve --srcQueue
queue:work
Start a BullMQ queue worker that processes jobs from named queues.
| Flag | Default | Description |
|---|---|---|
--queue <names> | default | Comma-separated queue names (e.g. default,mail,maintenance) |
--prod | — | Run from compiled dist/ instead of source |
--runtime <name> | node | Runtime: node or bun |
--src | — | Force source mode even if dist/ exists |
npm run maker queue:work
npm run maker queue:work -- --queue=mail
npm run maker queue:work -- --queue=default,mail,maintenance
npm run maker queue:work -- --prod
npm run maker queue:work -- --queue=default,mail --prod --runtime=nodepnpm maker queue:work
pnpm maker queue:work --queue=mail
pnpm maker queue:work --queue=default,mail,maintenance
pnpm maker queue:work --prod
pnpm maker queue:work --queue=default,mail --prod --runtime=nodeyarn maker queue:work
yarn maker queue:work --queue=mail
yarn maker queue:work --queue=default,mail,maintenance
yarn maker queue:work --prod
yarn maker queue:work --queue=default,mail --prod --runtime=nodebun maker queue:work
bun maker queue:work --queue=mail
bun maker queue:work --queue=default,mail,maintenance
bun maker queue:work --prod
bun maker queue:work --queue=default,mail --prod --runtime=nodequeue:clear
Delete all queue-related Redis keys. Useful for resetting stale job state during development.
npm run maker queue:clearpnpm maker queue:clearyarn maker queue:clearbun maker queue:clearScheduler
schedule:work
Start the cron scheduler worker. Executes scheduled tasks defined with defineSchedule() on their configured intervals.
npm run maker schedule:work
npm run maker schedule:work -- --prodpnpm maker schedule:work
pnpm maker schedule:work --prodyarn maker schedule:work
yarn maker schedule:work --prodbun maker schedule:work
bun maker schedule:work --prodTesting
test
Run Vitest backend tests once and exit.
Pass anything after the command straight through to Vitest, so all of Vitest's own options work here — --coverage, --reporter=verbose, --bail, and so on.
npm run maker test
npm run maker test -- posts
npm run maker test -- --reporter=verbosepnpm maker test
pnpm maker test posts
pnpm maker test --reporter=verboseyarn maker test
yarn maker test posts
yarn maker test --reporter=verbosebun maker test
bun maker test posts
bun maker test --reporter=verboseFiltering is positional, not a flag
To run part of the suite, name it — maker test posts matches test files whose path contains posts. There is no --filter option; Vitest has no such flag and errors with Unknown option \--filter`` if you try one.
The npm form needs one separator, not two
npm run maker test -- posts is right. npm run maker test -- -- posts also works, but for a confusing reason: npm consumes the first -- and forwards the second, which arrives as Vitest's argument separator and reads as a filter matching nothing. Because that quietly runs the whole suite instead of the part you asked for, it looks like it worked.
pnpm, yarn and bun need no separator at all.
test:watch
Run Vitest in watch mode — re-runs when a file changes.
npm run maker test:watch
npm run maker test:watch -- postspnpm maker test:watch
pnpm maker test:watch postsyarn maker test:watch
yarn maker test:watch postsbun maker test:watch
bun maker test:watch postsThis one keeps running
test runs once and exits; test:watch stays open until you stop it, which is why test:run is the one to reach for when you want an answer and an exit code.
test:coverage
Run Vitest once with a code coverage report.
npm run maker test:coverage
npm run maker test:coverage -- authpnpm maker test:coverage
pnpm maker test:coverage authyarn maker test:coverage
yarn maker test:coverage authbun maker test:coverage
bun maker test:coverage authThe report is written to coverage/ — a text summary in the terminal plus an HTML report you can open at coverage/index.html. That directory is gitignored, so it never shows up in a commit.
This reports; it does not judge
A new project has no coverage threshold, so this always succeeds and simply tells you the number. That is deliberate: the percentage includes the whole framework, not just your code, so a threshold calibrated for the framework itself would fail your project on its first run with every test passing.
See Testing for how to set one that is worth having once you have written enough tests.
test:ui
Run Vitest backend tests in the visual UI mode.
npm run maker test:uipnpm maker test:uiyarn maker test:uibun maker test:uiDev Tools
maildev:view
Start MailDev — a combined SMTP server and web email viewer. Catches all outgoing emails during development.
| Service | Port |
|---|---|
| SMTP | 1089 |
| Web UI | 1080 |
npm run maker maildev:viewpnpm maker maildev:viewyarn maker maildev:viewbun maker maildev:viewredis:view
Start Redis Commander — a web UI to browse and manage Redis keys.
| Service | Port |
|---|---|
| Web UI | 1369 |
If Redis is not reachable, the view does not fail silently. redis:view first probes the configured REDIS_URL and, when nothing answers (or REDIS=false in .env), it serves a Redis Commander Unavailable page on port 1369 naming the Redis URL it tried, instead of leaving the browser to report "unable to connect".
This page is specific to Redis Commander. It is not the queue dashboard: queue and job state live on the app's own bull-board route at /queues, which shows its own Queue Dashboard Unavailable page when Redis is down. The two tools are separate — Redis Commander browses raw Redis keys, the bull-board shows BullMQ queue state.
npm run maker redis:viewpnpm maker redis:viewyarn maker redis:viewbun maker redis:viewvite:cache:clear
Clear Vite caches. Removes .vite directories from node_modules, src/resources, and other locations.
npm run maker vite:cache:clearpnpm maker vite:cache:clearyarn maker vite:cache:clearbun maker vite:cache:clearCustom Commands
Your project can add its own maker commands. The files you own are maker/commands.mjs (plain JavaScript) or maker/commands.ts (TypeScript) in the project root.
It is deliberately outside src/. Anything under src/framework/ is copied into the published package, so an edit there is lost on the next framework update, and src/modules/ is swept by the route, job and seeder globs.
maker:init
Creates maker/commands.mjs with a working example. It will not overwrite an existing file unless you pass --force. It always creates the .mjs — to write your commands in TypeScript instead, create maker/commands.ts yourself and delete the .mjs, as described under TypeScript commands.
npm run maker maker:initpnpm maker maker:inityarn maker maker:initbun maker maker:initWriting them
program is the same commander object the built-in commands are built with, so .command(), .description(), .option() and .action() behave exactly as they do everywhere else in the maker CLI.
// maker/commands.mjs
import { sendInvoice } from "../src/invoices/send.js";
export default function register(program) {
program
.command("invoice:send <customer>")
.description("Send an invoice to a customer")
.option("--dry-run", "Print what would be sent")
.action(async (customer, options) => {
await sendInvoice(customer, { dryRun: options.dryRun });
});
}maker invoice:send acme --dry-run
maker --help # your commands appear alongside the built-insExport a default function, or a named register export — either is accepted.
TypeScript commands
Name the file maker/commands.ts instead of maker/commands.mjs and the loader reads it through tsx, with your project's tsconfig.json applied — so @/ imports and your path aliases work exactly as they do inside src/:
// maker/commands.ts
import { sendInvoice } from "@/invoices/send.js";
export default function register(program) {
program
.command("invoice:send <customer>")
.description("Send an invoice to a customer")
.option("--dry-run", "Print what would be sent")
.action(async (customer, options) => {
await sendInvoice(customer, { dryRun: options.dryRun });
});
}Everything above still applies — same exports, same commander object, same error behaviour, with maker/commands.ts named in the messages instead.
maker/commands.mjsnever touches tsx. The tsx hooks are registered only when the.tsfile is the one being loaded, once per process. Plain-JavaScript projects and production installs without devDependencies are therefore unaffected; a project that usesmaker/commands.tsneedstsxin its devDependencies (the scaffold includes it).- Actions can import TypeScript too. The hooks stay registered for the life of the process, so an action handler can
await import("@/modules/…/something.js")at dispatch time. - Your editor type-checks this file — the build does not.
maker/stays out of the root tsconfig'sinclude: adding it would change the common source directory and relocate every emitted file underdist/. Instead the scaffold shipsmaker/tsconfig.json— extends the root config,noEmit,rootDir: ".."— and that is the config your editor picks up for anything undermaker/, so@/paths, node types and imports fromsrc/all resolve with no red squiggles.build:backend'stsc -p tsconfig.jsonnever reads it, sodist/is untouched; at run time tsx transpiles without type-checking, and biome still lints the file.
If your project predates 4.2.2, create that file yourself — it is the whole thing:
{
"extends": "../tsconfig.json",
"compilerOptions": { "noEmit": true, "rootDir": ".." },
"include": ["./**/*.ts"]
}Four things that will bite you
Absent is silent. If neither maker/commands.mjs nor maker/commands.ts exists, nothing happens and nothing is printed. That is the common case, so it has to be free.
One file at a time. If both exist, maker/commands.mjs wins — it is the incumbent, and a project already using it must not change behaviour because a .ts file appeared. The .ts is not left silently dead: maker prints which file it ignored and why.
maker/commands.ts is ignored because maker/commands.mjs exists. Keep one - delete the file you are not using.Broken is loud. If the file exists but cannot be parsed, throws while loading, or exports the wrong shape, maker stops with an error naming the file — maker/commands.mjs or maker/commands.ts — and exits non-zero. It is never swallowed — a developer's commands silently not existing is far worse than a message. Every failure names the file:
Could not load maker/commands.mjs: Unexpected end of input
maker/commands.mjs must export a default function (program, args) => void. Found: undefined.A broken file blocks every maker command
This includes the framework's own. Until the file parses, module:make and the rest are unavailable too — the CLI cannot know which of your commands were supposed to work.
To get out, fix the file, or delete it. Deleting leaves nothing behind: nothing caches a registration and no framework update refers to it, so maker returns immediately to exactly the state it was in before maker maker:init.
Framework names win. Commander refuses to register a command whose name is already taken, so a project cannot shadow a built-in it did not write. The error names both the file and the name, so rename yours:
Could not register commands from maker/commands.mjs: cannot add command
'module:list' as already have command 'module:list'Stubs are not overridable
A custom command does not get a custom stub. module:make-* reads from src/framework/maker-cli/stubs/, which is framework-owned and replaced on every update. If you need your own template, write the file yourself from your command — that is the honest trade, and it keeps the generated file under your control.
Finding a command
maker --help shows the nexwire wordmark, then groups the commands by what you are trying to do, so you can answer a question instead of reading a flat list of every name.
When you will not see the wordmark
The wordmark is decoration, so it stays out of the way rather than insisting on itself:
- A terminal narrower than 41 columns gets plain
nexwire makertext instead. Art that overflows wraps into an unreadable mess. - Piped or redirected output gets plain text.
maker --help > help.txtproduces a file a human can read, not a file that opens with ASCII art. - Per-command help gets no wordmark at all.
maker module:make --helpis for checking a flag, not for branding.
Getting started
dev Start API + Vue 3 UI plus optional workers/tools
serve Start HTTP server (dist first if built, else src)
Generators - global
middleware:make Generate a middleware file in src/middlewares
Generators - modules
module:make Create one or more modules with facade, schema, service...
module:make-model Generate a model file for an existing module
Modules - manage
module:list List all discovered modules
module:delete Move a module directory to storage trash (soft delete)
Database
db:migrate Generate then run Drizzle migrationsThe full set of sections is Getting started, Generators - global, Generators - modules, Modules - manage, Database, Queue and scheduler, Deploy, Testing, and Your commands.
Generators - global is not the same as Generators - modules
middleware:make writes to src/middlewares and applies to the whole app. module:make-middleware writes inside a single module and applies only to it. They are near-identical names doing different things, so they are deliberately listed in different sections.
The listing shows command names and descriptions only. For the options and arguments of one command:
npm run maker <command> --helppnpm maker <command> --helpyarn maker <command> --helpbun maker <command> --helpmaker help <command> does the same thing and still works.
Why your own commands appear under "Your commands"
Commands you register in maker/commands.mjs or maker/commands.ts are listed beside maker:init, not mixed in with the built-ins, so it is obvious which commands you added.
Summary
| Command | Development | Production |
|---|---|---|
dev | Full stack (API + UI + queue + tools) | — |
serve | API with watch | API without watch |
queue:work | Queue worker from source | Queue worker from dist |
schedule:work | Scheduler from source | Scheduler from dist |
ui:dev | Vite dev server | — |
test | Run tests once | — |
test:watch | Run tests in watch mode | — |
test:coverage | Run tests with coverage | — |
test:ui | Run tests in UI mode | — |
queue:clear | Development cleanup | — |
maildev:view | Email testing | — |
redis:view | Redis inspection | — |
vite:cache:clear | Cache cleanup | — |
maker:init | Create maker/commands.mjs | — |
