CLI
@miiajs/cli provides commands for developing, building, and running MiiaJS applications across Bun, Deno, and Node.js. It also includes code generation and an interactive project scaffolding wizard.
Installation
bun add -D @miiajs/cli
npm install -D @miiajs/cli
pnpm add -D @miiajs/cli
yarn add -D @miiajs/cli
Commands
miia dev
Start the development server with hot reload and type checking:
miia dev
miia dev --runtime bun
miia dev --entry src/server.ts --env-file .env.local
Runs two parallel processes:
- TypeScript compiler in watch mode (
tsc --noEmit --watch) - Development server with file watching
If the server process crashes (uncaught throw, process.exit(1), bad import), the CLI automatically restarts it with a 500 ms debounce while keeping type-checking alive. A crash-loop guard gives up after 5 crashes within 10 seconds to avoid burning CPU on a genuinely broken file. A tsc crash, by contrast, always triggers a full shutdown - type-checking is part of the dev contract.
| Flag | Default | Description |
|---|---|---|
--runtime, -r | auto-detected | Runtime: bun, deno, node |
--entry | src/main.ts | Entry point file |
--env-file | .env (if exists) | Environment file path |
miia build
Build the project for production:
miia build
miia build --runtime node
| Runtime | Behavior |
|---|---|
| Bun / Deno | tsc --noEmit (type-check only) |
| Node.js | tsc (full compilation to dist/) |
| Flag | Default | Description |
|---|---|---|
--runtime, -r | auto-detected | Runtime: bun, deno, node |
miia start
Start the production server:
miia start
miia start --runtime node --dist dist/app.js
miia start --env-file .env.production
| Runtime | Command |
|---|---|
| Bun | bun src/main.ts |
| Deno | deno run --allow-all src/main.ts |
| Node.js | node dist/main.js |
Sets NODE_ENV=production for Bun and Node.js. Unlike dev, build, and check, the start command does not require tsc on PATH - production containers can ship without TypeScript installed.
| Flag | Default | Description |
|---|---|---|
--runtime, -r | auto-detected | Runtime: bun, deno, node |
--entry | src/main.ts | Source entry point |
--dist | dist/main.js | Compiled entry (Node.js only) |
--env-file | .env (if exists) | Environment file path |
miia check
Type-check the project:
miia check
Runs tsc --noEmit. No flags.
miia generate
Generate individual artifacts inside an existing project. Alias: miia g.
miia generate <schematic> <name> [flags]
miia g <alias> <name> [flags]
Schematics
| Schematic | Alias | Generates | Auto-registers in |
|---|---|---|---|
module | m | @Module class | parent module imports |
controller | c | @Controller class | parent module controllers |
service | s | @Injectable class | parent module providers |
resource | r | module + controller + service (CRUD) | parent module imports |
middleware | — | Middleware function | — (manual registration) |
guard | — | @Injectable CanActivate class | parent module providers |
Examples
Generate a controller with auto-registration:
miia g c user
CREATE src/user/user.controller.ts
UPDATE src/app/app.module.ts
+ import { UserController } from '../user/user.controller.js'
+ controllers: [..., UserController]
Generate a full CRUD resource:
miia g r product
CREATE src/product/product.module.ts
CREATE src/product/product.controller.ts
CREATE src/product/product.service.ts
UPDATE src/app/app.module.ts
+ import { ProductModule } from '../product/product.module.js'
+ imports: [..., ProductModule]
The resource schematic generates a module with pre-wired controller and service. The controller includes POST, GET, GET :id, PATCH :id, DELETE :id endpoints. The module is registered in the parent module's imports array.
Generate a flat service (no subdirectory):
miia g s auth --flat
CREATE src/auth.service.ts
Preview without writing files:
miia g c user --dry-run
Flags
| Flag | Default | Description |
|---|---|---|
--path | — | Subdirectory under src/ |
--flat | false | Skip creating a subdirectory |
--dry-run | false | Preview without writing files |
Parent module discovery
The CLI walks up from the generated file's directory toward src/, looking for the nearest *.module.ts. If none is found via walk-up, it checks src/app/app.module.ts as a fallback. Both src/app.module.ts and src/app/app.module.ts layouts are supported.
If no parent module is found, the file is still created - you just get a warning to register it manually.
File collisions
Generation is all-or-nothing: before writing anything the CLI checks that none of the target files exist. If any file already exists, the whole operation is aborted with exit code 1 and no files are touched. This applies both to single-artifact schematics and to resource (module + controller + service) - you won't end up with a half-generated directory.
Automatic formatting
After writing new files the CLI detects a local formatter in the project root and runs it on the generated output:
biome.json/biome.jsonc→./node_modules/.bin/biome format --write ....prettierrc*/prettier.config.js→./node_modules/.bin/prettier --write ...
Only local binaries in node_modules/.bin are used - nothing is fetched via npx. If the config exists but the binary is not installed, formatting is silently skipped. If no formatter config is present, the files are left in magicast's default style and you can format them with your own tooling afterwards.
Name with path segments
Names can include slashes to create nested directories:
miia g c auth/user
CREATE src/auth/user/user.controller.ts
miia new
Create a new MiiaJS project with an interactive wizard:
miia new my-app
miia new # prompts for name
miia new my-app --dry-run
miia new my-app --skip-install
The wizard walks you through:
- Project name (if not provided as argument)
- Runtime — Bun (recommended), Deno, or Node.js
- Package manager — pnpm (default), npm, or yarn (skipped for Bun)
- Features — multi-select from Config, JWT Auth, Swagger, CORS, Serve Static
- Database — single-select: Drizzle (PostgreSQL/MySQL/SQLite), Papr, Mongoose, or None
Features
| Feature | What it adds |
|---|---|
| Config | @miiajs/config + Zod env schema (src/env.schema.ts) |
| JWT Auth | @miiajs/auth + @miiajs/jwt + JWT/Local strategies + auth controller |
| Swagger | @miiajs/swagger + Swagger UI at /docs |
| CORS | Built-in cors() middleware in main.ts |
| Serve Static | @miiajs/serve-static + public/ directory |
| Drizzle + PostgreSQL | @miiajs/drizzle + drizzle-orm + postgres driver |
| Drizzle + MySQL | @miiajs/drizzle + drizzle-orm + mysql2 driver |
| Drizzle + SQLite | @miiajs/drizzle + drizzle-orm + better-sqlite3 driver |
| Papr + MongoDB | @miiajs/papr + papr + mongodb driver |
| Mongoose + MongoDB | @miiajs/mongoose + mongoose |
Features that need configuration (JWT Auth, all databases) automatically select Config if you haven't already.
A .env file is only written when a selected feature declares environment variables (JWT Auth, all database features). For feature-less projects the CLI skips .env entirely.
Generated structure
With Config + JWT Auth + Drizzle PostgreSQL + CORS selected:
my-app/
├── package.json
├── tsconfig.json
├── .gitignore
├── .env
└── src/
├── main.ts
├── env.schema.ts
├── types/
│ └── core.d.ts
├── app/
│ ├── app.module.ts
│ ├── app.controller.ts
│ └── app.service.ts
└── auth/
├── auth.module.ts
├── auth.service.ts
├── auth.controller.ts
└── strategies/
├── jwt.strategy.ts
└── local.strategy.ts
The generated app.module.ts comes pre-wired with all selected features:
import { Module } from '@miiajs/core'
import { ConfigModule } from '@miiajs/config'
import { envSchema } from '../env.schema.js'
import { JwtModule } from '@miiajs/jwt'
import { AuthModule } from '../auth/auth.module.js'
import { DrizzleModule } from '@miiajs/drizzle'
import { ConfigService } from '@miiajs/config'
import { AppController } from './app.controller.js'
import { AppService } from './app.service.js'
@Module({
imports: [
ConfigModule.configure({ schema: envSchema }),
JwtModule.configure((resolve) => ({
secret: resolve(ConfigService).getOrThrow('JWT_SECRET'),
expiresIn: '1h',
})),
AuthModule,
DrizzleModule.configure((resolve) => ({
dialect: 'postgres',
connection: { url: resolve(ConfigService).getOrThrow('DATABASE_URL') },
})),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
Flags
| Flag | Default | Description |
|---|---|---|
--dry-run | false | List files and deps without writing anything |
--skip-install | false | Create files but skip dependency installation |
The --dry-run preview lists runtime dependencies alongside devDependencies (marked with (dev)) so build-time packages like drizzle-kit, @types/better-sqlite3, or tsx are visible before you commit.
Runtime detection
The CLI automatically detects your runtime by checking lockfiles:
| Lockfile | Runtime |
|---|---|
bun.lock / bun.lockb | Bun |
deno.lock | Deno |
package-lock.json / yarn.lock / pnpm-lock.yaml | Node.js |
If no lockfile is found, it checks for bun or deno in PATH. Falls back to Node.js.
Override with --runtime:
miia dev --runtime bun
Only bun, deno, and node are accepted. Any other value fails fast with a clear error listing the valid options.
Package.json scripts
The generated project includes these scripts:
{
"scripts": {
"dev": "miia dev",
"build": "miia build",
"start": "miia start",
"check": "miia check"
}
}
Testing
Generate command
Create a test project and verify generation works:
miia new test-app # select Bun, Config, no DB
cd test-app
miia g r user --dry-run # preview CRUD resource
miia g r user # create user module + controller + service
miia dev # verify app starts with the new route
Verify the output:
curl http://localhost:3000/user # GET - returns []
curl -X POST http://localhost:3000/user # POST - returns creation stub
Scaffold command
Preview what would be generated:
miia new my-app --dry-run
Test with all features:
miia new full-app # select all features + a database
cd full-app
miia check # should report 0 type errors
miia dev # should start without runtime errors
Prerequisites
| Tool | Required by |
|---|---|
typescript (tsc) | dev, build, check (not start) |
tsx | Node.js dev command |
Runtime (bun / deno) | When using that runtime |
Production deployments only need the target runtime and your compiled/source entry file - tsc can be omitted from the final image.