Skip to content
Warlock.js v4

CLI commands

The warlock binary is the single entry point for everything you do at the terminal — dev server, production build, database operations, code generation, storage management. This page is the canonical reference for every shipped command, plus the recipe for adding your own.

Every command is a CLICommand instance produced by the command() factory. Built-ins, plugins from @warlock.js/* packages, and project-local commands all share the same shape. Knowing one is knowing all of them.

In a Warlock project, warlock is wired into package.json via yarn:

Terminal window
yarn warlock dev
yarn warlock migrate --fresh
yarn warlock generate.module products

Equivalent shortcuts your project may have configured:

Terminal window
yarn start # → yarn warlock dev
yarn migrate # → yarn warlock migrate
yarn build # → yarn warlock build

Look at the project’s package.json > scripts to see the local shortcuts. Under the hood everything routes through the same CLI runner.

warlock --help prints all commands. warlock <command> --help prints flags for one. The --help / -h flags are reserved — you can’t bind them yourself.


Start the development server with hot module reload, type generation, and health checkers.

Terminal window
warlock dev
warlock dev --fresh # clear the manifest before starting
warlock dev --skip-typings # skip background type generation
warlock dev --skip-health # skip health checkers
FlagTypeDescription
--fresh, -fbooleanDelete .warlock/manifest.json before starting (force full re-parse from disk).
--skip-typings, -stbooleanSkip background type generation for this run.
--skip-health, -shbooleanSkip file health checkers for this run.

Persistent — the process stays alive until you Ctrl+C it. Boots the full app: env, all configs, every connector, then your modules. See How it works for what’s happening behind the scenes.

Regenerate the TypeScript ambient types in .warlock/typings/ from your config files.

Terminal window
warlock generate.typings
warlock generate.typings --files src/config/database.ts,src/config/storage.ts
FlagTypeDescription
--files, -fstringComma-separated list of files to generate typings for. Omit to regenerate everything.

The dev server runs this automatically on every boot (unless --skip-typings). You’d run it manually after editing a config file in a fresh checkout or when the IDE’s autocomplete is lying to you.


Bundle the application for production. Reads warlock.config.ts > build for esbuild options.

Terminal window
warlock build

No flags. Output lands in dist/ (or wherever warlock.config.ts > build.outDir points). See How it works for the bundler story.

Start the production server from the build output.

Terminal window
warlock start

Persistent. Reads warlock.config.ts to resolve the entry path and source-map flag, then spawns node with the right arguments. Forwards SIGTERM / SIGINT to the child cleanly — Ctrl+C does what you expect.

Build first, then start:

Terminal window
warlock build && warlock start

Run database migrations. Without flags, runs all pending migrations.

Terminal window
warlock migrate
warlock migrate --fresh # drop all tables, run from scratch
warlock migrate --rollback # roll everything back
warlock migrate --list # show executed migrations only
warlock migrate --all # show all migration files in the app
warlock migrate --sql --pending-only # export pending migrations as SQL files
warlock migrate --sql --compact # ...minus comments and blank lines
warlock migrate --path src/app/orders/models/order/migrations
FlagTypeDescription
--fresh, -fbooleanDrop all tables and re-run migrations from scratch.
--rollback, -rbooleanRoll back migrations, dropping all tables.
--path, -pstringPath to a specific migration file or folder. Defaults to running all.
--list, -lbooleanList all executed migrations.
--all, -abooleanList every migration file in the app, executed or not.
--sql, -sbooleanExport migrations as phase-ordered SQL files instead of executing them.
--pending-onlybooleanWith --sql, export only pending migrations. Otherwise exports all.
--compact, -cbooleanWith --sql, strips generated comments and blank lines.

The minimal preload — only database and logger connectors — keeps migrations fast.

Run database seeds.

Terminal window
warlock seed
warlock seed --fresh # drop all rows first
warlock seed --list # show the order without running
warlock seed --transaction # run inside a transaction (default: true)
FlagTypeDefaultDescription
--fresh, -fbooleanDrop all rows from all tables before seeding.
--list, -lbooleanPrint the seed execution order without running.
--transaction, -tbooleantrueRun all seeds inside a single transaction.

Full bootstrap: true — seeds touch app models, so the whole app initializes. Slower than migrate; that’s the price for being able to use model classes in your seed files.

Create a new database on the configured connection.

Terminal window
warlock create-database my_app
warlock create-database my_app --connection secondary
warlock cdb my_app # alias
PositionalDescription
<name>The database name to create. Required.
FlagDefaultDescription
--connection, -c"default"Database connection name as defined in src/config/database.ts.

The connector layer takes care of MongoDB vs Postgres — your config decides; the command runs against whichever you’ve wired.

Drop every table in the database. Yes, every one. Use carefully.

Terminal window
warlock drop.tables
warlock drop.tables --force # skip confirmation
FlagDescription
--force, -fSkip the confirmation prompt.

Without --force, the command prompts before deleting anything. Pairs naturally with migrate --fresh if you want a complete reset.


Install one or more Warlock feature packages and run their setup.

Terminal window
warlock add auth
warlock add auth mail storage
warlock add --list # see what's available
warlock add auth --package-manager yarn
PositionalDescription
<features...>One or more feature names. Required unless using --list.
FlagDescription
--package-manager, -pmPackage manager to use (auto-detected if omitted).
--list, -lList every available feature.

The command installs the npm package(s), runs their post-install hooks (configuration files, migrations, etc.), and updates your warlock.config.ts where needed.


Upload a local file or directory to any configured storage driver.

Terminal window
warlock storage.put ./public/logo.png assets/logo.png
warlock storage.put ./uploads --driver r2 # whole directory
warlock storage.put ./uploads backups/2026 --driver r2 -c 10 # under a prefix
warlock sput ./public/logo.png assets/logo.png # alias
PositionalDescription
<localPath>The local file or directory to upload. Required.
[destination]Destination path/prefix. Optional — defaults to the file’s basename for files.
FlagTypeDefaultDescription
--driver, -dstringconfigured defaultStorage driver name as defined in src/config/storage.ts.
--concurrency, -cnumber5Number of concurrent uploads when uploading a directory.

Auto-detects file vs. directory. Files are streamed (no full-buffer-in-memory), so you can upload large assets without blowing the heap. Built for the “migrate from local storage to S3/R2” workflow.


The whole generate.* family lives in Generators — that page covers what each one produces, naming transformations, and best practices. The quick reference:

CommandAliasWhat it generates
generate <generator> [args...]gMaster dispatch — warlock g module products.
generate.module <name>gen.mA new module folder with the standard subfolders.
generate.controller <module>/<name>gen.cA controller, optionally with schema + request type.
generate.service <module>/<name>gen.sA service.
generate.model <module>/<name>gen.mdA Cascade model with its migration file.
generate.repository <module>/<name>gen.rA RepositoryManager subclass.
generate.resource <module>/<name>gen.rsA Resource subclass.
generate.validation <module>/<name>gen.vA schema, optionally with the request type.
generate.migration <model-path>gen.migA migration file with add/drop/rename DSL support.

See Generators for the full surface.


Three places to surface a custom CLI:

  1. Project-local — drop <name>.command.ts under src/app/<module>/commands/. Auto-discovered.
  2. Package-exported — your package exports registerXCommand(); the consuming project lists it under warlock.config.ts > cli.commands.
  3. Framework — built into @warlock.js/core. You don’t write these.
src/app/users/commands/promote-admin.command.ts
import { command } from "@warlock.js/core";
export default command({
name: "users.promote",
description: "Promote a user to admin by email",
alias: "up",
preload: {
env: true,
config: ["database"],
connectors: ["database", "logger"],
},
options: [
{
text: "--email, -e",
description: "User email address",
required: true,
},
],
action: async ({ options }) => {
console.log(`Promoting ${options.email}`);
// …business work using framework services
},
});

Run it: yarn warlock users.promote --email=hasan@example.com (or yarn warlock up -e hasan@example.com).

Two rules for project-local commands:

  • Default-export the result of command(...). The discovery loader does (await import(path)).default.
  • Don’t put logic at module top level. The file gets imported during scans (and again at execution time). Anything outside action/preAction runs at scan time, possibly before any config or env is loaded.
FieldTypeRequiredNotes
namestringyesDot notation OK (db.seed, jwt.generate). May include positional placeholders (name <arg>).
descriptionstringShown in help output.
aliasstringShort name (m for migrate).
preloadCLICommandPreloadWhat subsystems to load before action. See below.
persistentbooleantrue for long-running commands. Skips the auto-exit.
preAction(data) => void | PromiseRuns before preloaders — banner, input validation.
action(data) => void | PromiseyesRuns after preloaders. data is { args, options }.
optionsCLICommandOption[]Flag definitions.
{
text: "--fresh, -f", // "--key", "-k", "--key, -k", or "-k, --key"
description: "Drop tables first",
type: "boolean", // "string" (default) | "boolean" | "number"
defaultValue: false, // applied if flag missing
required: false, // 1 missing required → command refuses to run
}

The parser auto-extracts name (long form, camelCased) and alias (short form) from text. Inside action, read via options.fresh — kebab-case becomes camelCase (--no-cacheoptions.noCache).

Commands run with a minimal world by default. Opt in to what you need so the command stays fast:

preload: {
env: true, // load .env
config: ["database", "log"], // load these src/config/*.ts files (or `true` for all)
bootstrap: true, // full bootstrap (env + app + prestart hooks)
connectors: ["database", "cache"], // start these connectors (or `true` for all early-phase)
prestart: true, // run src/app/prestart.ts after config
warlockConfig: true, // load warlock.config.ts
runtimeStrategy: "production", // force-set
environemnt: "production", // force-set (note: original typo preserved in the API)
}

Connector names: "logger", "mailer", "http", "database", "cache", "storage", "communicator" (herald), "socket". Pass connectors: true to start every Early-phase connector. The http and socket connectors are Late phase and stay off unless you explicitly list them.

Picking the right preload matters: migrate only needs database and logger; seed needs the full bootstrap because seeds use app models. Inspect @warlock.js/core/src/cli/commands/*.command.ts for canonical pairings.

action: async ({ args, options }) => {
// args: positional, e.g. `warlock storage.put ./uploads backups/` → ["./uploads", "backups/"]
// options: flags, e.g. `--driver=r2 --concurrency=5` → { driver: "r2", concurrency: 5 }
};

For positional capture in name, declare slots: name: "storage.put <localPath> [destination]"<> required, [] optional. Slots are documentation/help-output; the registered command name is always the first whitespace-separated token.

The convention for external packages is a factory function that returns a fresh CLICommand:

@warlock.js/auth/src/commands/jwt-secret-generator-command.ts
import { command } from "@warlock.js/core";
import { generateJWTSecret } from "../services/generate-jwt-secret";
export function registerJWTSecretGeneratorCommand() {
return command({
name: "jwt.generate",
description: "Generate a JWT secret key in .env",
action: generateJWTSecret,
});
}

Wire it in the project’s warlock.config.ts:

warlock.config.ts
import {
registerAuthCleanupCommand,
registerJWTSecretGeneratorCommand,
} from "@warlock.js/auth";
import { defineConfig } from "@warlock.js/core";
export default defineConfig({
cli: {
commands: [
registerJWTSecretGeneratorCommand(),
registerAuthCleanupCommand(),
],
},
});

Why a factory and not the instance directly? Two reasons. It defers side effects (config loading, import work) until the command is wired in. And each project decides which commands it wants — listing the factory is opt-in.

By default, the framework prints Executing <name>…, runs your action, prints Done in <ms>ms, and process.exit(0). Throwing exits with 1 and prints the error. If persistent: true, the framework keeps the process alive — no auto-exit on success; errors are logged but don’t crash.

For colored output, use the colors helper re-exported from @warlock.js/core:

import { colors } from "@warlock.js/core";
console.log(colors.green("[OK]") + " user promoted");
console.log(colors.red("[!]") + " something went wrong");
  • Connectors are not free. connectors: true boots the database, cache, storage, etc. For a print-version-and-exit command, leave preload undefined.
  • Required options block execution. If required: true and the user omits the flag, the framework prints Missing required options: and exits 1 before action runs.
  • Aliases must be unique. The first registration wins; later collisions silently overwrite. If your alias mysteriously runs the wrong command, check for a project-local override of a framework alias.
  • name with positional slots is documentation. name: "storage.put <localPath>" registers as storage.put — the slot is for help output only.
  • --help is reserved. Don’t try to bind it. The framework intercepts it.