Skip to content

Create Warlock

create-warlock is the official project scaffolder. One command and a short setup flow later you have a complete, configured Warlock project that boots: choose API-only or full-stack web, then a database unless a database flag already supplied it. Prefer the old style? --interactive restores the full long-form wizard.

Migrating an existing app? Start with the v5 migration guide.

Terminal window
npm create warlock@latest my-app
cd my-app

With a terminal, that command asks for the project name (if you did not pass one), API-only or full-stack web, and a database unless --db or --no-db already supplied it. The database selector offers MongoDB and PostgreSQL, shows MySQL as disabled, and includes None for a database-free app. Everything else — package manager, optional features, git, JWT secrets — takes a sensible default, printed as a summary after scaffolding, with the flag to change each one. See Headless / non-interactive mode below for scripting it fully, or --interactive for the question-by-question flow.

What lands is a ready-to-run app: the src/app (one folder per feature) + src/config (one file per subsystem) layout, a warlock.config.ts, decorators already enabled in tsconfig.json, and a dev server you start with npm run dev.

create-warlock is fully headless: every prompt has a flag that answers it, --yes takes the default for anything you leave unset, and it works with no terminal at all — CI, a script, an agent. There are three modes:

  1. Default (a TTY, no --yes, no --interactive) — asks the project name (if not already given), API-only or full-stack web (--stack), then a database unless --db or --no-db already supplied it. The stack is the only choice that changes the generated app’s shape.
  2. --yes (or no TTY at all) — fully headless. Every answer comes from a flag or its default; nothing is ever prompted, and stdin is never read.
  3. --interactive (alias --customize) — the full long-form wizard, one prompt at a time: project name, package manager, database driver, features, AI providers, git, JWT.
Terminal window
# Scripting it in CI, or driving it from an agent: one command, zero prompts
npm create warlock@latest my-app -- --yes \
--db=postgres --pm=pnpm --stack=web \
--features=test,redis --ai=ai-openai \
--git --jwt --agents=claude,cursor
# The one structural fork, still headless
npm create warlock@latest my-web-app -- --yes --stack=web
# The full long-form wizard, one prompt at a time
npm create warlock@latest -- --interactive

The first positional argument (or --name) is the project name. Value flags accept either --db=postgres or --db postgres. Unknown --db, --features, --ai, --pm, --agents, or --lint values fail fast — before anything is written to disk — so a typo (or a hostile value) never leaves you with a half-scaffolded project.

FlagTakes valueDefaultPurpose
<positional> / --nameyes— (required)Project name + target directory
--stackyesapiapi or web — the structural choice; web seeds web, shadcn, and design-system by default
--db / --no-dbyes / nomongodbDatabase driver — mongodb, postgres, or none (--no-db is shorthand for --db=none)
--pmyesinferred (npm_config_user_agent or the system)Package manager — must be npm / yarn / pnpm / bun; any other value is rejected before scaffolding starts
--featuresyes (CSV)none, or ["web", "shadcn", "design-system"] when --stack=web and --features is not givenComma-separated optional feature keys
--lintyesoxcoxc (Oxlint/Oxfmt); removed or invalid values fail before writes
--aiyes (CSV)noneComma-separated AI provider keys (auto-pulls @warlock.js/ai)
--agentsyes (CSV)claudeComma-separated agent-kit targets to derive docs/skills for — validated against the published target list, cached after the first lookup
--git / --no-gitnooffForce-enable or force-disable git initialization
--jwt / --no-jwtnooffForce-enable or force-disable JWT secret generation
-y, --yesnooffSkip every prompt and accept defaults for anything unset
--interactive, --customizenooffRestore the full long-form interactive wizard
-h, --helpno—Print usage (every flag + its default) and exit
-v, --versionno—Print the installed create-warlock version and exit

-h/--help and -v/--version short-circuit before anything else runs — no prompt, filesystem write, or network call, and --help wins even over a positional project name.

Without a terminal, --yes is not even required — when stdin isn’t a TTY (CI, a script, a container, an agent harness), the scaffolder never tries to prompt, regardless of which mode was requested. If the flags already answer everything it needs, it just scaffolds. Prompting is the fallback, not the precondition.

The one thing that can’t fall back to a default is the project name. With no TTY and no name given (neither positional nor --name), the run fails immediately and names exactly what to pass instead of hanging on a prompt nobody can answer:

A project name is required and no terminal is available to ask for one. Pass it as
the first argument or --name=<name>. Non-interactive flags: --yes, --pm=<npm|yarn|pnpm>,
--db=<driver>|--no-db, --features=<list>, --ai=<list>, --lint=<oxc>, --git|--no-git, --jwt|--no-jwt.

AI remains optional in the long-form wizard. Press Enter with no AI selections to skip it. The AI multiselect also places None last; choosing it alone has the same result. None cannot be combined with an AI provider or capability package, so the wizard shows a warning and returns to the AI selector with the non-None selections retained for correction. Headless --ai flags are unchanged.

Exit codeWhen
0Success — including --help, --version, and a user cancelling an interactive prompt (Ctrl+C / Esc).
1A validation failure (unknown --pm/--db/--features/--ai/--agents/--lint value, missing project name with no TTY to ask), the target directory already exists, or the scaffold itself failed partway through (dependency install, git init, feature addition, or cache warm-up).

A scaffold failure surfaces the error and halts — there is no automatic rollback of partially-written files.

The toolchain leaves the framework’s own rules to warlock check, which runs the same with or without Oxlint. It reports request.all() reaching a query or service, mutating routes with no auth middleware and no public: true, redirects to request-supplied targets without safeRedirect(), raw emails and tokens in logs, unbounded regexes on request input, await log.* and .js import extensions, each with a WL code and its fix. The scaffold itself passes every check: its sign-in and password-recovery routes say public: true and are throttled, and its user list validates its query. The generated AGENTS.md tells coding agents to run npx warlock check after every change.

Oxlint 1.88.0 and Oxfmt 0.73.0 are the only supported tools. New apps pin TypeScript 7.0.2 and include .oxlintrc.json, .oxfmtrc.json, .oxfmtignore, and VS Code settings for oxc.oxc-vscode. --lint=oxc is optional; --lint=eslint fails with the 5.34.0 removal message. The wizard has no toolchain choice.

Terminal window
npm create warlock@latest my-app -- --yes
cd my-app
npm run lint:check
npm run format:check

Oxlint enables "categories": { "correctness": "error" }, unused-variable/import checks, explicit class accessibility and type-import style. Oxfmt sorts imports with "sortImports": { "newlinesBetween": false, "internalPattern": ["app/", "web/", "@shared/"] }. It keeps built-ins, packages, internal aliases and relative imports in one block. Formatting retains unused imports; lint reports them. Local package scripts use oxlint, oxfmt and tsc directly. The non-mutating scripts are lint:check and format:check. Background checks run TypeScript and configured Oxlint.

Not every app needs a database. Pick None in the wizard’s database step — or pass --db=none (--no-db is a shorthand) — and the scaffold leaves the database out entirely: no driver is selected, no driver package is installed, and the template’s src/config/database.ts is removed. Warlock’s database connector is gated on that config file, so the app boots cleanly with no database wired.

Terminal window
# A database-free API in one command
npm create warlock@latest my-api -- --yes --no-db --features=test

With --pm=pnpm, create-warlock reads pnpm --version and writes the pnpm settings that version understands, so esbuild’s install script (which warlock dev and warlock build need) is allowed and installs succeed:

pnpmWhat the app gets
10.26+, 11, 12pnpm-workspace.yaml with allowBuilds (and minimumReleaseAgeExclude for @warlock.js/*)
10.5 – 10.25the same file, plus an onlyBuiltDependencies list, which these versions read instead
7 – 9, 10.0–10.4no pnpm-workspace.yaml (these versions reject one without packages:); pnpm.onlyBuiltDependencies in package.json

Inside an existing pnpm workspace the app gets neither: the parent workspace’s settings apply. If you upgrade pnpm later, move the approvals to the row for the new version.

  • Full walkthrough → Core · Installation — every question, the scaffolded file tree, and booting the dev server, step by step.
  • Your first endpoint → Core · Getting Started — from a fresh project to a working route.
  • Scaffold as you build → Core · Generators — warlock generate.module, generate.controller, and the rest of the family.

Default Oxlint/Oxfmt projects pin typescript to 7.0.2; tsc is the native Go compiler. The template uses module: "ESNext", moduleResolution: "bundler", explicit Node types, and paths relative to its tsconfig, without baseUrl. Existing apps must remove baseUrl, rebase alias targets, and replace moduleResolution: "node" / "node10" before typechecking.

ESLint and Prettier support was removed in 5.34.0. Oxlint/Oxfmt is the only supported toolchain. --lint eslint and healthCheckers.eslint are removed. Install the new core release and ts-morph, then migrate an existing app:

Terminal window
npm install --save-dev ts-morph
npx warlock upgrade --from 5.33 --to 5.34.0 --dry-run
npx warlock upgrade --from 5.33 --to 5.34.0
npm install
npm run format
npm run lint:check
npx warlock check

The oxc-toolchain-removal step reapplies the preserved 5.33 oxc-toolchain migration when legacy configs, scripts or dependencies remain. Apps upgrading from 5.32 can still use npx warlock upgrade --from 5.32 --to 5.33.0 first. It replaces standard scripts/dependencies, pins TypeScript 7.0.2, updates generated editor settings, removes healthCheckers.eslint and selects Oxlint. A generated .prettierignore becomes .oxfmtignore; Oxfmt scripts read it explicitly.

Literal custom rules are translated without executing the config. Per-folder no-restricted-imports and no-restricted-exports become Oxlint overrides with file/exclusion patterns. Supported @typescript-eslint/* rules become native typescript/* rules (or native base rules). Options are checked against the pinned Oxlint 1.88.0 schema. Type-aware equivalents enable options.typeAware and add oxlint-tsgolint@7.0.2004 to the app’s dev dependencies. unused-imports/no-unused-imports uses no-unused-vars with safe import fixes; Oxlint shares the severity for imports and variables, so differing active severities are reported for review.

Only unsupported rules/options, computed or imported settings, selectors such as no-restricted-syntax, and conflicting existing Oxc rules need manual review. Each finding names its source file:line, rule and affected file patterns. Fully ported lint configs are removed; configs with findings remain for reference until those findings are addressed. Existing Oxc rules are kept on conflict. --dry-run --json reports the same plan without writes. Customized formatting, editor settings and nonstandard scripts can also require review.

Upgrading: new and migrated template configs enable "categories": { "correctness": "error" }. Address the additional diagnostics; existing Oxc configs retain their categories. A real lockfile beside a workspace marker determines the installer: package-lock.json plus pnpm-workspace.yaml without pnpm-lock.yaml now produces npm install.

Generated apps include nine project skills for API design, TypeScript standards, persistence, Git workflow, module boundaries, resilience, security, testing, and web standards. Each discovery description stays within 450 characters and names a sibling skill for adjacent work. References use skill names and package topic names so agent-kit can resolve the installed project and package skill names.

New domain fields and resource output use camelCase; driver-managed timestamp storage can retain snake_case. The sample User and Post models keep Cascade’s numeric primary keys internally and generate a separate publicId UUID server-side. Their resources expose that value as id; post update routes and user ID filters resolve it through publicId. Cascade’s uuid getter only stringifies a primary key. For legacy stored columns, a resource schema entry such as totalCents: ["total_cents", "int"] emits camelCase without renaming the database column.

Both model schemas declare publicId with v.string().uuid().default(() => randomUUID()), using randomUUID from node:crypto. The callback generates a fresh UUID for each record; the migrations enforce uniqueness with string(36).unique().

The HTTP config combines errorMapper for opaque unexpected thrown errors with errorResponse for every JSON error response. The public contract is { error: { code, message }, details?, requestId }, including framework-generated 404, 422, 429, and 500 responses. Validation issues appear under details; 5xx messages stay opaque and diagnostic payloads/stacks are omitted. Routed errors carry Warlock’s real request ID; early Fastify rejections use its request ID, echoed in the same correlation header. HTML error pages and streams keep their own contracts. See HTTP response formatting.

The contact, locale, user create/list and post create/update examples declare responseSchema for generated client types and OpenAPI; the API design skill requires this declaration for new controllers. Side-effecting API contracts require Idempotency-Key; authenticate before idempotency middleware, enforce the header in app middleware, and use shared storage for retries across processes. The contact demo remains a side-effect-free validation example. Its locale interaction, field wiring, and submit state live in feature hooks, with one component per file and shared props types.

Oxlint enables typescript/no-explicit-any, explicit member accessibility, curly, and no-console. The hybrid interface-for-contracts/type-for-data convention stays a review rule because consistent-type-definitions only supports one global preference. App sub-folder barrels remain permitted; web imports target individual component and hook files to keep client bundles lean and server-only code out of barrels. Run npm run lint:check and npm run format:check in the generated app.

These model/resource conventions apply to newly generated apps. Existing apps adopting public UUIDs must add and backfill a unique column before changing their resource output or route lookups; updating a skill alone does not migrate stored data.

The generated warlock.config.ts configures build.budgets.page with gzip limits of 150kb JavaScript and 50kb CSS in mode: "error". API-only builds have no client pages to check. Adjust the limits or choose "warn" as needed; see client performance budgets.

Full-stack web projects include e2e/home.e2e.ts. Run npx warlock e2e with Chrome or Edge installed. The spec builds and starts an owned app copy, visits / at 1280×800 and 390×844, then calls page.assertAccessible(), page.assertViewport() and page.screenshot("home"). API-only scaffolds omit this web spec. The fixture excludes dotenv files and production data directories; provide disposable service configuration explicitly if you add database-backed features. A database-free scaffold needs no external database.

npm create warlock@latest my-project -- --features=test uses warlock add test to generate separate Vitest projects. unit discovers src/**/*.spec.ts outside tests/ with no framework or HTTP setup. integration discovers *.test.ts and specs under tests/ with the framework setup and a worker cleanup runner. The template pins Vitest 5.0.3 for that runner API. Existing configs are preserved.

Terminal window
npx vitest run --project unit
npx warlock test --changed --since "$LAST_RELEASE_REF"
npx vitest run --project integration
# Full suite nightly:
npx vitest run

The default runtime lifetime is per file. For reuse, profiling and the quota-aware recommendedTestWorkers() helper, see the testing guide. The generated testing-strategy project skill covers the same trade-offs.

The full-stack preset (--stack=web) now seeds web, shadcn, and design-system. Selecting shadcn in Customize or passing --features=tailwind,shadcn also adds the design system. An explicit --features replaces the stack’s default selection.

The generated app receives fourteen accessible Mongez controls under src/web/design-system/components, with separate hooks and props types. Each component is a default export imported through the web alias, for example import EmailInput from "web/design-system/components/email-input";. Forms use Form directly from @mongez/react-form. shadcn primitives and exact runtime dependency versions are copied into the app; existing source is preserved.

See Shared form controls for the component list, props, value shapes, keyboard and date semantics. Existing Tailwind apps can use npx warlock add design-system.

Starter controllers declare responseSchema for their success and explicit error statuses. Keep those contracts aligned when changing controller responses, including the token payloads in authentication controllers.

The interactive feature picker uses compact labels and drops hints when space is limited, including the existing-Tailwind design controls option on a 40-column terminal.

When submitting a generated form asynchronously, return the save promise from onSubmit so Mongez clears its submitting state on completion and allows another submission. A synchronous handler must manage that state explicitly.