Docs / CLI

CLI

Every hopak command, flag, and scaffolded output.

The hopak CLI is the only thing that writes source files to disk — models, routes, CRUD, migrations, dev certs. There are no config flags that cause the runtime to materialize code or crypto on your behalf. This page lists every command.

Install

bun add -g @hopak/cli

You get a hopak binary on your $PATH. Use this for the hopak new <name> flow and any ad-hoc command.

As a dev dependency

bun add -d @hopak/cli

Call via bunx hopak <cmd> or through package.json scripts. Pins the CLI version with your project — good for teams and CI.

hopak new <name>

Scaffolds a new Hopak project in ./<name>/. The directory must not exist. By default the dialect is SQLite — zero-install, works offline, file stored at .hopak/data.db. Pick a different DB up front with --db:

hopak new my-app                   # SQLite (default)
hopak new my-app --db postgres     # Postgres — installs `postgres` driver
hopak new my-app --db mysql        # MySQL — installs `mysql2` driver
hopak new my-app --db sqlite       # explicit opt-in (same as default)

Flags:

FlagEffect
--db <sqlite|postgres|mysql>Preconfigures hopak.config.ts, adds the driver to package.json, seeds .env.example with a DATABASE_URL placeholder.
--no-installSkips bun install — useful for CI or offline setups.

Generates (SQLite default):

my-app/
├── app/
│   ├── models/post.ts             # example model — edit fields to taste
│   ├── routes/index.ts            # GET /
│   └── routes/api/
│       ├── posts.ts               # GET list + POST create (uses crud.*)
│       └── posts/[id].ts          # GET/PUT/PATCH/DELETE (uses crud.*)
├── public/                        # static files
├── hopak.config.ts                # database: { dialect: 'sqlite', ... }
├── main.ts                        # await hopak().listen()
├── tsconfig.json
├── package.json                   # depends on @hopak/core, dev-depends on @hopak/cli
├── .gitignore
├── .env.example
└── README.md

With --db postgres / --db mysql the database: block reads { dialect: 'postgres', url: process.env.DATABASE_URL }, the driver (postgres or mysql2) is added to dependencies, and .env.example contains a DATABASE_URL=… placeholder.

After scaffolding:

cd my-app
# sqlite: just run —
hopak dev

# postgres / mysql: fill in the connection first —
cp .env.example .env
# edit DATABASE_URL, then:
hopak sync      # CREATE TABLE IF NOT EXISTS for every model
hopak dev

hopak sync is the bootstrap path for new projects. The moment you need schema evolution (adding a column to an existing table), switch to migrations — hopak migrate init captures the current state, and hopak migrate new <name> is the canonical flow from there on. sync refuses to run once app/migrations/ has files.

hopak dev

Runs the project with Bun’s --hot mode plus a lightweight file watcher on app/ so newly-added model or route files trigger a cold-restart (Bun’s own hot reload only patches already-imported modules).

hopak dev

On start:

  Hopak.js v0.5.0
  ↳ Listening on http://localhost:3000
  ↳ Database: sqlite

Edits to existing files reload through Bun’s HMR with state preserved (milliseconds). Adding / deleting a route or model file logs New/removed file under app/ — restarting… and the dev child is respawned.

Press Ctrl-C to stop.

hopak generate <kind> [<name>]

Scaffolds files from a template. Aliased as hopak g. Four kinds:

KindWhat it writesArg
model <name>app/models/<name>.ts (one table)required
crud <name>app/routes/api/<plural>.ts + app/routes/api/<plural>/[id].ts (REST for the model)required
route <path>app/routes/<path>.ts (one handler)required
cert.hopak/certs/dev.{key,crt} + local .gitignore (for local HTTPS)none

hopak generate model <name>

hopak generate model comment
# → Created file  app/models/comment.ts
// app/models/comment.ts
import { model, text } from '@hopak/core';

export default model('comment', {
  name: text().required(),
});

Edit the fields to match your domain. Generating the model alone gives you a typed client via ctx.db.model('comment'); a DB table follows on the next hopak sync (or first hopak dev boot) on a project without migrations — if the project uses migrations, write hopak migrate new add_comments + hopak migrate up. Then hopak generate crud comment scaffolds the HTTP endpoints.

hopak generate crud <name>

hopak generate crud post
# → Created file  app/routes/api/posts.ts
# → Created file  app/routes/api/posts/[id].ts

Each generated file uses the crud helpers exported from @hopak/core:

// app/routes/api/posts.ts
import { crud } from '@hopak/core';
import post from '../../models/post';

export const GET = crud.list(post);
export const POST = crud.create(post);
// app/routes/api/posts/[id].ts
import { crud } from '@hopak/core';
import post from '../../../models/post';

export const GET = crud.read(post);
export const PUT = crud.update(post);
export const PATCH = crud.patch(post);
export const DELETE = crud.remove(post);

Six endpoints on /api/<plural>/ — all paginated, validated, and with sensitive fields (password / secret / token) stripped. Customize a verb by replacing its export with your own defineRoute(...); delete an export to remove the verb entirely (the router answers 405 Method Not Allowed with an Allow: header listing what remains).

hopak generate route <path>

hopak generate route search
hopak generate route posts/[id]/publish
hopak generate route api/users/[id]
hopak generate route files/[...rest]

Creates the file at the given path under app/routes/. Leading / and trailing .ts are stripped — posts/new.ts and posts/new both produce app/routes/posts/new.ts. The template starts with a GET handler:

import { defineRoute } from '@hopak/core';

export const GET = defineRoute({
  handler: (ctx) => ({ ok: true, path: ctx.path }),
});

Rename the export to any verb, or add multiple exports in one file.

hopak generate cert

hopak generate cert
# → Generating self-signed dev certificate { path: ".hopak/certs" }
# → Dev certificate ready. Re-run `hopak dev` with HTTPS enabled.

Shells out to openssl req -x509 once, writes the key/cert/gitignore trio, and exits. Idempotent — running it when both files already exist is a no-op. Pair with server.https.enabled: true in hopak.config.ts; hopak dev refuses to start with HTTPS enabled but no cert files present and points you back here.

Requires openssl on the machine. macOS ships it; on Linux: apt install openssl / apk add openssl.

Refusal policy

hopak generate model/route/crud never overwrites. If any target already exists the command fails with exit code 1. generate cert is the exception — it’s idempotent and exits 0 when the files are already there.

hopak use <capability>

Enables a capability in an existing project. One command installs any packages, patches the right files, and adds env keys.

Run with no arguments to see what’s available:

hopak use
# Usage: hopak use <capability>
#
# Available:
#   sqlite       SQLite via bun:sqlite (default, zero install)
#   postgres     Postgres via postgres.js
#   mysql        MySQL via mysql2
#   request-log  Per-request logging — tags each request with an id and logs method/path/status/ms
#   auth         JWT auth — signup/login/me routes + requireAuth() middleware

Database dialects — sqlite / postgres / mysql

Switch dialects: installs the driver (postgres / mysql2), rewrites the database: block in hopak.config.ts, and adds DATABASE_URL to .env.example.

hopak use postgres
hopak use mysql
hopak use sqlite

The patcher replaces a bare-default block (what hopak new writes) in place, but refuses to touch a block you’ve tuned (custom file path, extra URL params, ssl config) — it prints the snippet to paste and exits 1, so tuning is never silently discarded.

For a brand-new project, prefer hopak new <name> --db <dialect> — it’s one fewer step.

request-log

Patches main.ts so every request gets a correlation id and a log line. Goes from:

import { hopak } from '@hopak/core';

await hopak().listen();

to:

import { hopak, requestId, requestLog } from '@hopak/core';

await hopak().before(requestId()).after(requestLog()).listen();

Subsequent runs detect that requestId + requestLog are already in the chain (by factory name, not exact call) and report Already using request-log — safe to run in setup scripts. If main.ts has drifted from the template, the patcher refuses and prints the snippet to paste.

See the core docs recipe for format options ('simple' / 'json') and custom id generators.

auth

Scaffolds JWT credential auth in one command. Creates:

app/middleware/auth.ts           # exports requireAuth + signToken
app/routes/api/auth/signup.ts    # POST /api/auth/signup
app/routes/api/auth/login.ts     # POST /api/auth/login
app/routes/api/auth/me.ts        # GET /api/auth/me (requires token)
app/models/user.ts               # created only if you don't already have one

It also adds JWT_SECRET to .env.example and runs bun add @hopak/auth jose.

hopak use auth
# → files created, deps installed
# → next: copy .env.example → .env, set JWT_SECRET
#
# no migrations yet:
#   hopak sync && hopak dev
#
# migrations already in use:
#   hopak migrate new add_users
#   # fill in up/down with CREATE TABLE users (...)
#   hopak migrate up && hopak dev

If any scaffolded file already exists, the command refuses to overwrite it and prints the snippet so you can merge by hand. See Auth for the full API (OAuth providers, RBAC, claim extension).

hopak sync

Create missing tables from the current models — dev bootstrap. Emits CREATE TABLE IF NOT EXISTS for every registered model and CREATE INDEX IF NOT EXISTS for each .index() field, topologically sorted so FK targets come before dependents.

hopak sync
Syncing schema to database {"cwd":"/.../my-app"}
Schema synchronized {"models":3,"dialect":"sqlite"}

Safe to run repeatedly. Useful in CI, right after hopak use postgres on a fresh database, or before the first hopak dev on Postgres / MySQL. Does not ALTER existing tables — the moment model columns drift from the DB, sync prints a drift warning pointing at hopak migrate.

Once app/migrations/ exists, sync refuses to run and directs you to hopak migrate up so the two mechanisms never fight.

hopak migrate

Schema evolution with history and rollback. Aliased as hopak m. Subcommands:

CommandEffect
hopak migrate initGenerate initial migration from current models (one time)
hopak migrate new <name>Empty skeleton file with up/down
hopak migrate up [--to ID] [--dry-run]Apply pending migrations
hopak migrate down [--steps N] [--to ID] [--dry-run]Roll back (default: last 1)
hopak migrate statusApplied / pending / missing

Each migration is one .ts file in app/migrations/:

// app/migrations/20260422T160100_add_role.ts
import type { MigrationContext } from '@hopak/core';

export const description = 'Add role column to user';

export async function up(ctx: MigrationContext): Promise<void> {
  await ctx.sql`ALTER TABLE users ADD COLUMN role TEXT DEFAULT 'user'`;
}

export async function down(ctx: MigrationContext): Promise<void> {
  await ctx.sql`ALTER TABLE users DROP COLUMN role`;
}

ctx.db is the full Hopak db client — use it for data migrations (backfill columns, rewrite rows) alongside DDL in the same file.

SQLite + Postgres run each migration inside a transaction. MySQL auto-commits DDL, so split multi-step changes into separate files; drift partway through leaves partial state otherwise.

See Migrations for the full walkthrough.

hopak check

Audits project state without starting a server. Prints a coloured report of what Hopak will see at boot time: config, database location, models scanned, routes discovered, static directory. Validates the config up front and fails fast on an invalid dialect, port, or logLevel.

hopak check
  ✓ Config    hopak.config.ts loaded
  ✓ Database  sqlite (/.../my-app/.hopak/data.db)
  ✓ Models    3 loaded (comment, user, post)
  ✓ Routes    8 file route(s)
  ✓ Static    serving public/

Exits with 1 if any model/route file fails to scan, or if the config is invalid — safe to run in CI to catch broken scaffolds before they hit the dev server.

Project structure

The CLI relies on the default Hopak layout unless you override paths:

my-app/
├── app/
│   ├── models/       # hopak generate model <name> writes here
│   ├── routes/       # hopak generate route/crud writes here
│   └── migrations/   # hopak migrate init / new writes here
├── public/           # served as static files
├── .hopak/           # runtime state (db file, dev cert); gitignored
├── hopak.config.ts   # optional
└── main.ts           # entry point executed by hopak dev

hopak sync, hopak check, and hopak dev all read hopak.config.ts to locate these directories.

Custom project paths

Override source directories in hopak.config.ts:

import { defineConfig } from '@hopak/core';

export default defineConfig({
  paths: {
    models: 'src/domain',
    routes: 'src/api',
    public: 'static',
  },
});

After this, hopak generate model post writes to src/domain/post.ts, and hopak dev / hopak check look in the new locations.

Integration with package.json scripts

A scaffolded project comes with:

{
  "scripts": {
    "dev": "hopak dev",
    "start": "bun run main.ts"
  }
}

Typical extensions:

{
  "scripts": {
    "dev": "hopak dev",
    "start": "bun run main.ts",
    "sync": "hopak sync",
    "check": "hopak check",
    "test": "bun test"
  }
}

When @hopak/cli is listed as a devDependency, bun run dev invokes the local binary — no global install needed on that machine.

hopak —version / —help

hopak --version   # prints package version
hopak -v

hopak --help      # prints command overview
hopak -h
hopak             # (no args — same as --help)