# CLI

`okm` reads `okmodel.config.ts` from the directory it starts in. The `okmodel` bin is the same program. The file default-exports `defineConfig` from `okmodel/migrate`. Each option is a page under [Config](/docs/define/config).

`okm push` is for a database you can recreate. A shared database uses reviewed migrations: `okm generate init`, then `okm migrate apply`.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
});
```

## Run

Node.js 22.18 or newer loads the config. `bunx okm` uses Node. `bun --bun okm` uses Bun.

<Callout type="info" title="Node.js">
A process that cannot load a `.ts` config exits 1. The line is `okm needs Node.js >=22.18 to load a .ts config (type stripping). This process is <version>. Run bun --bun okm to use the Bun runtime.`
</Callout>

| Invocation | Result |
| --- | --- |
| No arguments, `help`, `--help`, or `-h` | Prints `okm <command>` and one line per command. |
| `okm <words> --help` | Prints the first command whose name is those words, or starts with them. `okm migrate --help` prints `migrate plan <name>`. An unknown name prints the list. |
| `--version` | Prints the package version and does not run a command. The Node.js check still runs first. |
| A failure | Exit code 1. Stderr is `error <code>: <message>` and a fix line. |
| `--verbose`, or `OKM_DEBUG=1` | That failure also prints the stack. |

An unknown command is `Unknown command <name>.` `okm migrate` with a subcommand other than `plan`, `apply`, `status`, or `check` is `Unknown command migrate.`

## Commands

| Command | Result |
| --- | --- |
| `build` | Writes the catalog under [out](/docs/define/config/out). |
| `catalog export` | Writes the catalog snapshot a dependent verifies. |
| `check` | Lints the schema. Compares one database when a target is selected. |
| `dev` | Names a target called `dev`, or creates a PGlite database and prints its directory. |
| `doctor [code]` | Lists triggers, checks [roles](/docs/define/config/roles) when they are set, or prints one error code. |
| `driver test <driver>` | Runs the conformance suites against one driver. |
| `ext list` | Lists extensions the server can install. |
| `ext check` | Compares declared extensions with the server. |
| `ext scaffold <name>` | Prints a starting `extension()` definition, including composite types. `--out` writes it and never overwrites. |
| `ext test <file>` | Checks that definition against the server. |
| `generate [name]` | Writes the next migration. |
| `import drizzle <path>` | Converts a Drizzle schema into table files. Runs that file. Needs no database. |
| `migrate plan <name>` | Prints the plan. Adds lock estimates when one target answers. `--json` prints a document. |
| `migrate apply` | Applies pending migrations. |
| `migrate status` | Prints each target's version and state. |
| `migrate check` | Replays migration history in a scratch schema and rolls it back. |
| `push` | Applies the schema diff. Writes no SQL file. |
| `pull` | Reads one database and writes one table file per table, plus the schema file. |
| `seed <file>` | Runs the file's default export on one target. |

## Flags

`check`, `generate`, `push`, `pull`, `migrate`, `seed`, `doctor`, and `ext` parse the flags in this table. `build` and `dev` leave the rest of the line unread. `driver test` reads `--url` only, in [driver test](#driver-test). `pull` reads `--target`, `--schema`, and `--out` itself.

| Flag | Read by |
| --- | --- |
| `--target <name>`, `--target=<name>` | `check`, `migrate plan`, `migrate apply`, `migrate status`, `migrate check`, `push`, `pull`, `seed`, `ext`, and `doctor` when [roles](/docs/define/config/roles) is set. Who requires it is [Database](/docs/define/config/database). |
| `--allow-protected` | `migrate apply`, `push`, and `seed`. One invocation. Blocked work is [Protection](/docs/define/config/database#protection). `migrate check` ignores the flag. |
| `--allow-pooler` | `migrate apply`, `push`, `seed`, `migrate check`, and `check` when it compares a database. Same opt-in as [allowPooler](/docs/define/config/pooler). Either one is enough. |
| `--lock-timeout <milliseconds>` | `migrate apply`. Overrides [timeouts.lock](/docs/define/config/timeouts). `0` disables it. |
| `--statement-timeout <milliseconds>` | `migrate apply`. Overrides [timeouts.statement](/docs/define/config/timeouts). |
| `--json` | `migrate plan`. Prints a JSON document instead of the SQL text. The lock strings match the human plan, including row estimates. |
| `--replace <table>.<column>.<old>=<new>` | `generate` and `migrate plan`. Repeat it for each removed value. `=null` is the null replacement, on a nullable column. |
| `--provision` | `migrate check`. Compares a snapshot install with a full replay. |
| `--schema <name>`, `--schema=<name>` | `pull`. Repeat it, or separate names with a comma. The default is the target's current schema. |
| `--out <directory>`, `--out=<directory>` | `pull` and `import drizzle`. Writes `schema.ts` and `tables/` there. Without it, the command uses the schema path and the tables directory from the config, or `schema.ts` and `tables/` when `import drizzle` has no config yet. |

A missing `--target` name is OKM1853: `--target needs a name.` A missing `--replace` value is OKM1541: `--replace needs table.column.old=new.` `--lock-timeout` and `--statement-timeout` need a finite number of milliseconds that is not negative. The message names the flag. `check`, `push`, and `migrate check` refuse a bad number and do not apply it. The same parser runs on the other commands in the list above.

## build

`okm build` validates the schema and writes the [out](/docs/define/config/out) directory. The default is `.okm`. It prints that directory. It does not connect.

The files are `catalog.json`, `catalog.hash`, and `types.d.ts`. `catalog.hash` is the SHA-256 of `catalog.json`. `types.d.ts` is the row types. The mode header is on [Types](/docs/define/schema/types).

```sh
bunx okm build
```

## pull

`okm pull` reads one database and writes one table file per table, plus the schema file. It does not write the selected namespaces, so a protected target is allowed. It creates a scratch schema to verify functions and views, then drops it. An existing file is OKM1855 and nothing is written. `--out <directory>` is the other place to write. The default namespace is the target's current schema. `--schema <name>` selects others and may be repeated.

Export bindings and file stems are unique when folded to lowercase, so `Tasks` and `tasks` become `Tasks` / `tasks_2` and do not collide on a case-insensitive volume or in row types. A Windows-reserved stem (`con`, `aux`, `nul`, `com1`–`com9`, `lpt1`–`lpt9`, any case) gets a `_` prefix. The SQL name stays in `sqlName` when the binding differs. A column named `__proto__` is written as a computed key. Single-column foreign keys name the exported table binding, not the raw SQL table name. When `oxfmt` is missing, the files stay unformatted, a notice is printed, and the report still returns.

Tables, columns, keys, uniques, checks, single-column foreign keys, indexes, identity sequences, enums, domains, extensions, functions, triggers, views, and materialized views are written. A view is SQL text with its columns. Views and `BEGIN ATOMIC` bodies take dependencies from `pg_depend`. A plpgsql or SQL-string body gets `dependsOn` candidates from a lexical match. The report marks each function `verified` (the scratch replay created it with `check_function_bodies` on), `candidate-only` (plpgsql: creation does not prove the body), or `unverified` (creation failed). A comment above the declaration says the same thing. Policies, roles, grants, aggregates, procedures, event triggers, and rules are printed under `not pulled yet` with a count, and so is anything `table()` cannot say. A column type with no builder is that list, with the table and the column. Nothing is dropped without a line.

```sh
bunx okm pull
bunx okm pull --schema public --out ./adopted
```

## import drizzle

`okm import drizzle <path>` reads a Drizzle schema and writes one table file per table, plus the schema file. It imports that path, so the file runs. The `drizzle-orm` package is the one installed in the project. okmodel does not depend on it. A missing package is OKM1856: install `drizzle-orm` in the project. An existing file is OKM1855 and nothing is written. The command does not connect.

The report starts with `namespace <name>`. When the name is not `public`, set [namespace](/docs/define/config/namespace) to it. `schema()` still compiles `public`. File stems follow the same lowercase-folded uniqueness and Windows-reserved rules as [pull](#pull). Composite primary-key columns are not null even when Drizzle omitted `.notNull()` on the column.

Composite foreign keys, `relations()`, and custom column types are printed under `not imported` with the table and the reason. A `serial` column is written as `integer` not null. The sequence `serial` would create is listed, not written.

```sh
bunx okm import drizzle ./src/db/schema.ts --out ./adopted
```

## catalog export

`okm catalog export` writes the column snapshot of the schema this project owns: table names plus each column's name, type, and nullability. It is not the full catalog; that is [`describe(schema)`](/docs/reference/schema-api). It does not connect. `--json` prints the JSON. `--out <file>` writes that file and prints the path. With neither, the JSON is printed. `--json` and `--out` together print the JSON and write the file.

The file has `catalogHash` and `logical` (`namespace`, `name`, `format`). A table is a namespace, a name, and columns. There is no function and no connection URL. A dependent lists the path in [`external`](/docs/define/config/external).

```sh
okm catalog export --out identity.okm.json
```

## check

`okm check` lints the schema against the previous snapshot, then prints warning findings and `ok`. An error finding is OKM1510 and the command exits 1. A finding line is `<severity> <code> <place>: <reason> -- fix: <fix>`. An external table is checked against each [`external`](/docs/define/config/external) snapshot and, when a target is selected, against that database. A stale or missing snapshot is OKM1841. A foreign key whose namespaces differ is a `coupling` warning. A foreign key into another database is OKM1842.

A [tables](/docs/define/config/tables) directory is scanned for a `.ts` file whose name is absent from the schema module. That is OKM1024: `Table file not in schema: <file>.` A schema that would validate on write, with no runtime import of `okmodel/validate`, is OKM1201. The rules are on [Validation](/docs/define/schema/validation).

With one configured target, or with `--target`, a database that already has `okm_meta` is compared with the schema. A difference is OKM1520: `The database differs from the schema.` Several targets and no `--target` skip that comparison. No target skips it too. An unknown `--target` is still OKM1845. A protection mismatch on the same database is OKM1852. Both cases are on [Database](/docs/define/config/database).

## dev

`okm dev` looks for a target named `dev`. It prints `target dev` when that target is configured. With no such target it creates a PGlite database at `<out>/dev-db`, runs `select 1`, closes it, and prints that directory. The default `out` is `.okm`. The command then returns. It does not watch the schema, does not reload, and does not apply migrations.

```sh
bunx okm dev
```

## driver test

`okm driver test <driver>` runs the conformance suites against one driver and prints a report. `<driver>` is `pglite`, `postgresjs`, `pg`, `bun`, `neon`, or a path to a module. The module exports `name`, `capabilities`, `open`, and `connect`. `memory: true` means one in-process connection. `notices: false` means the driver does not surface `RAISE NOTICE`. `size` is the pool size the stats case expects. Omitted, `memory` is false, `notices` is true, and `size` is 1 for an in-process driver and 4 otherwise. Bun must be `>=1.4.0` (`package.json` `engines.bun`); a lower Bun stops with a clear message. Each wire and error case drops its `okm_*` tables before the pool closes.

`--url <postgres-url>` is the database. `--url=<postgres-url>` is the same. `DATABASE_URL` is used when the flag is omitted. `pglite` does not connect to it. The other built-in drivers need one of the two. The command does not read `okmodel.config.ts`.

The report starts with `driver <name>` and `dialect postgres`. Then one row per case, tab-separated: case id, behavior, result, capability. The result is `pass`, `fail`, or `unsupported`. A case that does not run is `unsupported` (or `fail`), never `pass`. The capability is set on an unsupported row and names the limit (`cancel`, `describe`, `stream`, `listen`, `notices`, `one connection`, `no socket to cut`). A failure exits 1. An unsupported case does not.

PGlite has no socket to cut, so COMMIT loss is `unsupported` with capability `no socket to cut`. Neon uses the same row: the harness cut is a TCP relay, and Neon speaks WebSocket or HTTP.

```sh
okm driver test pglite
okm driver test postgresjs --url "$DATABASE_URL"
```

## doctor

`okm doctor <code>` prints one registry entry and does not load the project.

```text
OKM1520: Schema drift
The database has drifted past what an expand migration can repair.
Inspect the diff and repair the database, or generate a migration that matches the drift.
```

An unknown code is `Unknown code <code>.`

With no code, the command lists each table's triggers, one line `<table>: <name>, <name>`, in table order. No trigger is `no triggers`. When [roles](/docs/define/config/roles) is set, it also connects and checks external roles, `CREATEROLE` when a role is managed, and the application role's grants. A missing managed role is left for the plan. That connection is the read-only `check` class. A protected target is allowed. The command does not change the database.

```sh
bunx okm doctor OKM1520
```

## ext

`okm ext list` and `okm ext check` connect to the selected target as the read-only `check` class. A protected target is allowed. A pooler host is not refused.

`list` prints a tab-separated table. The header is `name`, `default`, `installed`, `schema`.

`check` compares extensions declared on the schema with what is installed. No declaration is `no extensions`. A missing install is OKM1811: `Extension <name> is not installed on the server.` A version the declaration rejects is OKM1812: `Extension <name> needs <declared> or higher. The server has <installed>. Install a version the declaration accepts, or change the pin.` An exact pin omits "or higher". A match prints `name`, `declared`, `installed`. An omitted pin is `-`. Declarations are on [Extensions](/docs/define/schema/extensions).

`scaffold <name>` reads `pg_depend`, `pg_type`, `pg_proc`, `pg_operator`, and `pg_settings` for an extension that is already installed. A missing install is OKM1811. The text is a starting `extension()` definition the project owns. `--out <file>` writes that text. An existing file is `invalid` and nothing is written. Without `--out` the text is printed.

`test <file>` loads that module and checks each exported `extension()`. Every declared type, operator, function, and index method must exist with that signature. A mismatch is `invalid` and the message lists each one. A `requires` version or Postgres floor the server does not meet is OKM1812. A match prints `ok <name>`.

Another name is `Unknown command ext <name>.`

```sh
bunx okm ext check
```

## generate

`okm generate` plans the schema against the last `*.catalog.json` in name order and writes SQL. The name defaults to `migration`. Letters, digits, `_`, and `-` are accepted. Anything else is `Migration name <name> must be letters, digits, _ or -.`

The file is `<number>_<name>.sql` in the [migrations](/docs/define/config/migrations) directory, with `<number>_<name>.catalog.json` beside it. The number is one higher than the highest numeric prefix, at least four digits. An empty directory starts at `0001`. The name `init` also writes `.gitattributes` in the project when that file is missing.

An empty plan writes no SQL file and prints `no changes`. The [out](/docs/define/config/out) catalog is still written. A written file prints its path, then one line per lint finding. An error finding does not fail this command. `migrate plan` and `check` exit 1 on the same finding.

`--replace` records a removed picklist or enum value. The schema stores no replacement. A chain, a value replaced by another removed value, is OKM1541. `=null` on a column that is not nullable is OKM1541.

```sh
bunx okm generate init
```

```sh
bunx okm generate rename-status --replace notes.status.draft=open
```

## migrate plan

`okm migrate plan <name>` prints the plan and writes nothing. A missing name is `okm migrate plan needs a name.`

With one target, or with `--target`, a database that answers adds `pg_class` row estimates to the lock lines. No target, several targets and no `--target`, or a target that cannot be reached, prints the plan without estimates. An unknown `--target` and a protection mismatch still throw. Those are the same refusals as [Database](/docs/define/config/database). A pooler host is not refused.

An error finding is OKM1510 and the command exits 1 after the plan text. `--json` prints one document and no SQL text: `name`, `class`, each step's `class`, `lock`, and `sql`, and the findings (`severity`, `code`, `place`, `reason`, `fix`). `lock` is the human `-- lock:` line. The command still exits 1 after that document when a finding is an error. `--replace` is the same flag as [generate](#generate).

```sh
bunx okm migrate plan init
```

## migrate apply

`okm migrate apply` applies pending files in the [migrations](/docs/define/config/migrations) directory. A file without its `.catalog.json` is refused. With no migration files, or no head snapshot, it prints `target <name>` and `nothing to apply` and does not connect.

A pending file is linted after `okm_history` is read and before any DDL. `lintFrom` is not read. An error finding is OKM1510. Transaction control is the finding OKM1551. Apply accepts that step when it carries `-- okm-allow OKM1551: reason`. An empty reason, or a code the statement did not trigger, is OKM1510. [migrate check](#migrate-check) still refuses the step.

The summary is `target <name>`, then `set role <role>` when apply issued `SET ROLE`, then `restamped <id>` for a history repair, then `applied <id>` for each file. Nothing applied is `nothing to apply`. A backfill batch is printed as it commits:

```text
backfill <migration> step <index> batch <n> rows <n> key <last>
```

`key` is the committed boundary, or `-` when that batch has none.

A protected target allows expand, reference rows, and provision of an empty database. Contract, unclassified SQL, a backfill, and a history repair that changes a row are OKM1850 until `--allow-protected`. An empty database, a database that already has objects and no history, and a provision that stopped part-way are on [Database](/docs/define/config/database#an-empty-database). Reference rows are on [Reference rows](/docs/define/table/reference-rows). Timeouts are on [Timeouts](/docs/define/config/timeouts). Batch size, pause, and the batch statement timeout are on [Backfill](/docs/define/config/backfill). Roles are on [Roles](/docs/define/config/roles).

A schema module that does not export a `schema()` still lets apply run. It installs no reference rows. The message is on [Schema](/docs/define/config/schema).

```sh
bunx okm migrate apply --target prod
```

## migrate status

`okm migrate status` prints every configured target, in name order. `--target` keeps one name. Several targets do not need `--target`. No target is OKM1845.

The columns are tab-separated: `target`, `version`, `catalog`, `state`, `protected`. `protected` is `true` or `false`. The state is `current`, `behind by expand`, `behind by contract`, `ahead by expand`, `ahead by contract`, or `failed at step <n> (resume with okm migrate apply)`.

An unfinished backfill follows as its own section: a line `backfill`, then the tab-separated header `migration`, `step`, `rows`, `key`, `state`.

A pooler host is not refused. The command is read-only, including on a protected target.

```sh
bunx okm migrate status
```

## migrate check

`okm migrate check` replays every migration into a scratch schema inside one transaction, then rolls that transaction back and drops the schema. The schema name starts with `okm_check_`. A passing run prints `ok <n> migrations`. A statement left unrun follows on its own line, `<id> step <n>: <reason>`. The reasons are `VACUUM is not run inside the check transaction`, `CREATE or DROP DATABASE is not run inside the check transaction`, and `ALTER SYSTEM is not run inside the check transaction`.

A protected target is OKM1850. The message is `okm migrate check writes a scratch schema and refuses protected target <name>. Point it at a throwaway Postgres.` `--allow-protected` does not lift it.

The command also checks the previous catalog, the head, and the linter. The first linted id is [lintFrom](/docs/define/config/lint-from). An error finding is OKM1510. Transaction control (`begin`, `commit`, `rollback`, `abort`, `end`, `start transaction`, `prepare transaction`), including inside a `DO` body, is that finding, OKM1551. A function or procedure body is not this. When the finding is allowed, the replay still throws OKM1551: `Migration <id> step <n> contains <word>.`

`--provision` also installs the head snapshot and compares it with a full replay. A difference is OKM1521: `Provisioning from the snapshot does not match the replayed history.`

[timeouts](/docs/define/config/timeouts) apply when the config sets them. The flags do not. A pooler host is refused until [allowPooler](/docs/define/config/pooler) or `--allow-pooler`.

```sh
bunx okm migrate check --provision
```

## push

`okm push` plans the schema against the live catalog on the target and applies that plan as one migration named `push-<n>`, where `n` comes from `okm_history`. It writes no SQL file. An empty plan that already matches `okm_meta` prints `target <name>` and `no changes`. When the hash moved with no DDL, it re-stamps `okm_meta` and prints `restamped push-<n>` then `nothing to apply`. A plan with steps uses the apply summary.

A protected target is OKM1850 until `--allow-protected`. Lock and statement timeouts stay 5000 and 30000 milliseconds. The config and the flags are not applied. A pooler host is refused until [allowPooler](/docs/define/config/pooler) or `--allow-pooler`.

```sh
bunx okm push
```

## seed

`okm seed <file>` loads that module and calls its default export with the testing harness for the selected target. The path is resolved from the project directory. A missing file is `Seed file <file> was not found.` A default export that is not a function is `<file> must default-export a function.` A missing file argument is `okm seed needs a file.`

The target is already migrated. The harness does not apply the catalog. A protected target is OKM1850 until `--allow-protected`. Several targets need `--target`. A pooler host is refused until [allowPooler](/docs/define/config/pooler) or `--allow-pooler`.

The command prints `target <name>` and one line `<table> <count>` for each table the factories inserted, in table name order. No insert is `created nothing`.

Under `rlsTenancy` the command runs as the application role. The owner, a superuser, or a `BYPASSRLS` role is OKM1707.

```ts title="seed.ts"
import type { Testing } from "okmodel/testing";

export default async function seed(t: Testing): Promise<void> {
  const f = t.factories({
    notes: (x) => ({ title: x.words(2) }),
  });
  await f.notes.create();
}
```

```sh
bunx okm seed seed.ts
```
