experimental

CLI

Dev server, build, code generation, project scaffolding, and more.

@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

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:

  1. TypeScript compiler in watch mode (tsc --noEmit --watch)
  2. 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.

FlagDefaultDescription
--runtime, -rauto-detectedRuntime: bun, deno, node
--entrysrc/main.tsEntry point file
--env-file.env (if exists)Environment file path

miia build

Build the project for production:

miia build
miia build --runtime node
RuntimeBehavior
Bun / Denotsc --noEmit (type-check only)
Node.jstsc (full compilation to dist/)
FlagDefaultDescription
--runtime, -rauto-detectedRuntime: 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
RuntimeCommand
Bunbun src/main.ts
Denodeno run --allow-all src/main.ts
Node.jsnode 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.

FlagDefaultDescription
--runtime, -rauto-detectedRuntime: bun, deno, node
--entrysrc/main.tsSource entry point
--distdist/main.jsCompiled 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

SchematicAliasGeneratesAuto-registers in
modulem@Module classparent module imports
controllerc@Controller classparent module controllers
services@Injectable classparent module providers
resourcermodule + controller + service (CRUD)parent module imports
middlewareMiddleware function— (manual registration)
guard@Injectable CanActivate classparent 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

FlagDefaultDescription
--pathSubdirectory under src/
--flatfalseSkip creating a subdirectory
--dry-runfalsePreview 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:

  1. Project name (if not provided as argument)
  2. Runtime — Bun (recommended), Deno, or Node.js
  3. Package manager — pnpm (default), npm, or yarn (skipped for Bun)
  4. Features — multi-select from Config, JWT Auth, Swagger, CORS, Serve Static
  5. Database — single-select: Drizzle (PostgreSQL/MySQL/SQLite), Papr, Mongoose, or None

Features

FeatureWhat 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
CORSBuilt-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

FlagDefaultDescription
--dry-runfalseList files and deps without writing anything
--skip-installfalseCreate 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:

LockfileRuntime
bun.lock / bun.lockbBun
deno.lockDeno
package-lock.json / yarn.lock / pnpm-lock.yamlNode.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

ToolRequired by
typescript (tsc)dev, build, check (not start)
tsxNode.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.