{"_id":"@alexify/migronaut","_rev":"2-805378da4258bee3523be1e4d2ff66bd","name":"@alexify/migronaut","dist-tags":{"latest":"2.0.0"},"versions":{"1.0.0":{"name":"@alexify/migronaut","version":"1.0.0","keywords":["mongodb","mongo","migration","migrations","mongodb-migration","mongodb-migrations","database-migration","schema-migration","migrate-mongo","mongoose","mongoose-migration","rollback","transactions","cli","typescript","nosql"],"author":{"name":"Alex Dolid","email":"dolid.sasha@gmail.com"},"license":"MIT","_id":"@alexify/migronaut@1.0.0","maintainers":[{"name":"alex-dolid","email":"dolid.sasha@gmail.com"}],"homepage":"https://migronaut.vercel.app/","bugs":{"url":"https://github.com/Alexis-Technologies/migronaut/issues"},"bin":{"migronaut":"bin/migronaut.js"},"tsd":{"directory":"tests/types"},"dist":{"shasum":"ebeca3c10347718d55f544ad876476f6295cd726","tarball":"https://registry.npmjs.org/@alexify/migronaut/-/migronaut-1.0.0.tgz","fileCount":50,"integrity":"sha512-eMbJE5elOghLmSOJpMSZHKs9qbCzSEiJR3LhOm8sxl73964uXaflxSZ2YALxADp7sOXTKyp+j3QKfJELZRTo4A==","signatures":[{"sig":"MEUCIQDd2mMlpXydi9P3u4aRUsluX5ktxzEVKMG/b8WfSz32ggIgQPelvrv4MMjkXzVco37re3tNB8BPEPtVDpdD9sRgk7U=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":322299},"main":"index.js","_from":"file:alexify-migronaut-1.0.0.tgz","types":"index.d.ts","engines":{"node":">=22.18.0"},"exports":{".":{"types":"./index.d.ts","default":"./index.js"}},"scripts":{"lint":"oxlint src bin scripts tests bench","size":"node scripts/size.js","test":"pnpm run test:unit && pnpm run test:integration","bench":"node bench/bench.js","format":"oxfmt src bin scripts tests bench","release":"pnpm publish","docs:dev":"vitepress dev docs","check:dts":"tsc --noEmit --strict --skipLibCheck false index.d.ts","test:unit":"node --test \"tests/unit/**/*.test.js\"","docs:build":"vitepress build docs","test:types":"tsd","docs:preview":"vitepress preview docs","format:check":"oxfmt --check src bin scripts tests bench","test:coverage":"c8 --all --include 'src/**' --check-coverage --lines 90 --branches 90 --functions 90 --reporter text --reporter lcov node scripts/node-test.js --test-concurrency=1 \"tests/unit/**/*.test.js\" \"tests/integration/**/*.test.js\"","test:integration":"node scripts/node-test.js --test-concurrency=1 \"tests/integration/**/*.test.js\""},"_npmUser":{"name":"alex-dolid","email":"dolid.sasha@gmail.com"},"_resolved":"/private/var/folders/9z/hn3k1glj0wz37h2hr379hr600000gn/T/67d02299c7b2e7ef0ba43200b7efad8a/alexify-migronaut-1.0.0.tgz","_integrity":"sha512-eMbJE5elOghLmSOJpMSZHKs9qbCzSEiJR3LhOm8sxl73964uXaflxSZ2YALxADp7sOXTKyp+j3QKfJELZRTo4A==","repository":{"url":"git+https://github.com/Alexis-Technologies/migronaut.git","type":"git"},"_npmVersion":"11.11.0","description":"Elegant, fast, fully-typed, zero-dependency MongoDB migrations for Node.js","directories":{"test":"tests"},"_nodeVersion":"24.14.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"c8":"^10.1.3","tsd":"^0.31.2","pino":"^10.3.1","oxfmt":"^0.60.0","oxlint":"^1.75.0","esbuild":"^0.28.1","mongodb":"^6.12.0","mongoose":"^8.9.2","vitepress":"^1.6.4","typescript":"^5.7.2","@types/node":"^22.19.19","mongodb-memory-server":"10.4.3"},"peerDependencies":{"mongodb":">=5.0.0","mongoose":">=7.0.0"},"peerDependenciesMeta":{"mongoose":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/migronaut_1.0.0_1785242651790_0.690298467197989","host":"s3://npm-registry-packages-npm-production"}},"2.0.0":{"name":"@alexify/migronaut","version":"2.0.0","description":"Elegant, fast, fully-typed, zero-dependency MongoDB migrations for Node.js — adopts an existing migrate-mongo changelog in one command","license":"MIT","author":{"name":"Alex Dolid","email":"dolid.sasha@gmail.com"},"repository":{"type":"git","url":"git+https://github.com/Alexis-Technologies/migronaut.git"},"homepage":"https://migronaut.vercel.app/","bugs":{"url":"https://github.com/Alexis-Technologies/migronaut/issues"},"engines":{"node":">=22.18.0"},"main":"index.js","types":"index.d.ts","bin":{"migronaut":"bin/migronaut.js"},"exports":{".":{"types":"./index.d.ts","default":"./index.js"}},"directories":{"test":"tests"},"publishConfig":{"access":"public"},"tsd":{"directory":"tests/types"},"keywords":["mongodb","mongo","migration","migrations","mongodb-migration","mongodb-migrations","mongodb-migrate","database-migration","schema-migration","migrate-mongo","migrate-mongo-alternative","mongoose","mongoose-migration","rollback","transactions","zero-dependency","cli","typescript","nosql"],"peerDependencies":{"mongodb":">=5.0.0","mongoose":">=7.0.0"},"peerDependenciesMeta":{"mongoose":{"optional":true}},"devDependencies":{"@types/node":"^22.19.19","@vercel/analytics":"^2.0.1","@vercel/speed-insights":"^2.0.0","c8":"^10.1.3","esbuild":"^0.28.1","mongodb":"^6.12.0","mongodb-memory-server":"10.4.3","mongoose":"^8.9.2","oxfmt":"^0.60.0","oxlint":"^1.75.0","pino":"^10.3.1","tsd":"^0.31.2","typescript":"^5.7.2","vitepress":"^1.6.4"},"scripts":{"test":"pnpm run test:unit && pnpm run test:integration","test:unit":"node --test \"tests/unit/**/*.test.js\"","test:integration":"node scripts/node-test.js --test-concurrency=1 \"tests/integration/**/*.test.js\"","test:coverage":"c8 --all --include 'src/**' --check-coverage --lines 90 --branches 90 --functions 90 --reporter text --reporter lcov node scripts/node-test.js --test-concurrency=1 \"tests/unit/**/*.test.js\" \"tests/integration/**/*.test.js\"","test:types":"tsd","check:dts":"tsc --noEmit --strict --skipLibCheck false index.d.ts","lint":"oxlint src bin scripts tests bench","format":"oxfmt src bin scripts tests bench","format:check":"oxfmt --check src bin scripts tests bench","size":"node scripts/size.js","bench":"node bench/bench.js","docs:dev":"vitepress dev docs","docs:build":"vitepress build docs","docs:preview":"vitepress preview docs","release":"pnpm publish"},"_id":"@alexify/migronaut@2.0.0","_integrity":"sha512-sywa96gTz58Mpb8Eij4TaJZQ9gyRzntUQ66xdcMOR84T80kvlpjGe8UDWn/5vsBubXKRi3mkuj9EUDHGKXWBVA==","_resolved":"/private/var/folders/9z/hn3k1glj0wz37h2hr379hr600000gn/T/50c9a787ed014fc3e5929a62eaba8cca/alexify-migronaut-2.0.0.tgz","_from":"file:alexify-migronaut-2.0.0.tgz","_nodeVersion":"24.14.1","_npmVersion":"11.11.0","dist":{"integrity":"sha512-sywa96gTz58Mpb8Eij4TaJZQ9gyRzntUQ66xdcMOR84T80kvlpjGe8UDWn/5vsBubXKRi3mkuj9EUDHGKXWBVA==","shasum":"f0257741b1740330148901f5da1555e753be1c93","tarball":"https://registry.npmjs.org/@alexify/migronaut/-/migronaut-2.0.0.tgz","fileCount":52,"unpackedSize":367303,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQC01VgdjWFiKpCv7jRL/OzZW7HCw7sFqAJC/WAFetxhEwIhAMmGHT6/n3ng0dxaqO50mIVWzG97KHHXlTUcasjiuGLO"}]},"_npmUser":{"name":"alex-dolid","email":"dolid.sasha@gmail.com"},"maintainers":[{"name":"alex-dolid","email":"dolid.sasha@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/migronaut_2.0.0_1788086674136_0.1834305587411056"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-28T12:44:11.635Z","modified":"2026-08-30T10:44:34.426Z","1.0.0":"2026-07-28T12:44:11.934Z","2.0.0":"2026-08-30T10:44:34.277Z"},"bugs":{"url":"https://github.com/Alexis-Technologies/migronaut/issues"},"author":{"name":"Alex Dolid","email":"dolid.sasha@gmail.com"},"license":"MIT","homepage":"https://migronaut.vercel.app/","keywords":["mongodb","mongo","migration","migrations","mongodb-migration","mongodb-migrations","mongodb-migrate","database-migration","schema-migration","migrate-mongo","migrate-mongo-alternative","mongoose","mongoose-migration","rollback","transactions","zero-dependency","cli","typescript","nosql"],"repository":{"type":"git","url":"git+https://github.com/Alexis-Technologies/migronaut.git"},"description":"Elegant, fast, fully-typed, zero-dependency MongoDB migrations for Node.js — adopts an existing migrate-mongo changelog in one command","maintainers":[{"name":"alex-dolid","email":"dolid.sasha@gmail.com"}],"readme":"<div align=\"center\">\n\n<img src=\"https://raw.githubusercontent.com/Alexis-Technologies/migronaut/main/assets/logo-icon-b-alt2.png\" alt=\"@alexify/migronaut\" width=\"420\" />\n\n# migronaut\n\n**Elegant, fast, fully-typed, zero-dependency MongoDB migrations for Node.js.**\n\n_Adopt an existing `migrate-mongo` changelog in one command — then get the controls it never had._\n\n[![npm](https://img.shields.io/npm/v/%40alexify%2Fmigronaut)](https://www.npmjs.com/package/@alexify/migronaut)\n[![CI](https://github.com/Alexis-Technologies/migronaut/actions/workflows/ci.yml/badge.svg)](https://github.com/Alexis-Technologies/migronaut/actions/workflows/ci.yml)\n[![node](https://img.shields.io/node/v/%40alexify%2Fmigronaut)](#quick-start)\n[![dependencies](https://img.shields.io/badge/runtime_dependencies-0-brightgreen)](#reasons-to-choose-it)\n[![docs](https://img.shields.io/badge/docs-online-blue)](https://migronaut.vercel.app/)\n[![license](https://img.shields.io/npm/l/%40alexify%2Fmigronaut)](./LICENSE)\n\n\nPrecise, safe migrations for MongoDB. Run a single file, roll back anything, and preview every\nchange before it touches your database.\n\n### 📖 [Read the documentation →](https://migronaut.vercel.app/)\n\n</div>\n\n> [!TIP]\n> ### 🔄 Already using `migrate-mongo`? Switch in under a minute.\n>\n> `migronaut` adopts your existing `changelog` **as-is** — no re-running migrations, no data loss, no rewriting\n> files. Point it at the same database and bring your whole history over in one command:\n>\n> ```bash\n> migronaut import     # one-time: adopt your migrate-mongo changelog (it's never modified)\n> migronaut up         # applies only what's new — your past migrations are recognized as already applied\n> ```\n>\n> Your applied history is preserved and new migrations run normally. Your `up`/`down`/`create`/`status`\n> mental model carries over 1:1 — you just gain dry-runs, single-file control, real rollbacks, hooks,\n> and locking. No `migrate-mongo` history at all? `migronaut baseline` adopts an existing database\n> with no prior tool. → **[See how it works](#advanced-features)**\n\n---\n\n## Reasons to choose it\n\n- **Zero dependencies** — no runtime dependencies at all; only the `mongodb` driver as a peer.\n  Instant installs, nothing extra in your lockfile, no supply-chain surface.\n- **Run a single migration** — `migronaut up <file>`, not just \"all pending\".\n- **Roll back anything** — a batch (`--batch 3`), the last N (`--steps 2`), one file, or `redo`.\n- **Preview before you run** — `migronaut dry-run up` prints the exact plan without touching the database.\n- **No race conditions** — an atomic MongoDB lock stops two deploys running migrations at once.\n- **Tamper detection** — SHA-256 checksums catch a migration edited after it was applied.\n- **Audit trail kept** — a rollback updates the record, it never deletes it.\n- **Adopt any existing database** — `migronaut import` for a migrate-mongo history,\n  `migronaut baseline` when there was no migration tool at all.\n- **Out-of-order detection** — a migration merged late from a parallel branch is flagged\n  (warn by default, `onOutOfOrder: 'error'` to refuse) instead of silently applying.\n- **Lifecycle hooks** — `beforeAll`, `afterAll`, `beforeEach`, `afterEach`, `onError`.\n- **Opt-in transactions** — wrap a migration so it fully commits or fully aborts.\n- **TypeScript, ESM & CommonJS** — all run with no `ts-node` plumbing.\n- **Zero config files required** — drive everything from env vars if you prefer.\n- **Pino-friendly logging** — the `logger` option is pino-compatible; pass a pino instance directly\n  and migronaut logs through it (with a `component: 'migronaut'` child binding).\n\n### How it compares to `migrate-mongo`\n\n| Capability                                      | `migrate-mongo` | `migronaut` |\n| ----------------------------------------------- | :-------------: | :-----------------: |\n| `up` / `down` / `create` / `status`             |        ✅        |          ✅          |\n| Run a single migration file                     |        ❌        |          ✅          |\n| Roll back a specific batch (not just the last)  |        ❌        |          ✅          |\n| Dry-run preview                                 |        ❌        |          ✅          |\n| `redo` (down + up)                              |        ❌        |          ✅          |\n| Concurrency lock                                | opt-in, TTL-only | ✅ always on: heartbeat, owner token, abort-on-loss, `lock`/`unlock` CLI |\n| Checksum / tamper detection                     | opt-in `useFileHash` (re-runs changed files) | ✅ enforced drift detection: per-row `checksumOk`, `--strict`, `audit` |\n| Lifecycle hooks                                 |        ❌        |          ✅          |\n| First-class TypeScript (built-in)               |        ❌        |          ✅          |\n| History preserved on rollback (never deleted)   |        ❌        |          ✅          |\n| Adopt an existing `migrate-mongo` changelog     |        —        | ✅ `migronaut import` |\n\n<sub>Reflects `migrate-mongo`'s documented CLI as of mid-2026 (v14: optional\n`lockCollectionName`/`lockTtl` lock, optional `useFileHash`). It has since added transaction access\nvia a `client` argument; `migronaut` exposes the same plus a declarative per-file `useTransaction` flag.</sub>\n\n### How it compares to `mongo-migrate-kit`\n\n`migronaut` is a fork of [`mongo-migrate-kit`](https://www.npmjs.com/package/mongo-migrate-kit)\n(CLI `mmk`) by Santosh Gupta. The fork was not a rename — everything below landed after it:\n\n| Capability                                      | `mongo-migrate-kit` | `migronaut` |\n| ----------------------------------------------- | :-------------: | :-----------------: |\n| Runtime dependencies                            |        6        |      **0**          |\n| Ships as                                        | bundled `dist/` | source, no build step |\n| `--json` on every command                       |        ❌        |          ✅          |\n| Typed exit code per error (`EXIT_CODES`)        |        ❌        |          ✅          |\n| `status --check` deploy gate                    |        ❌        |          ✅          |\n| `audit` / `lock` commands                       |        ❌        |          ✅          |\n| `up --to` / `down --to` targeting               |        ❌        |          ✅          |\n| Lifecycle events (`EventEmitter`)               |        ❌        |          ✅          |\n| Reuse an already-connected `MongoClient`        |        ❌        |          ✅          |\n| Changelog written inside the migration's transaction |   ❌        |          ✅          |\n| Credentials masked in errors, logs and `--json` |        ❌        |          ✅          |\n| Pino-compatible logger                          |        ❌        |          ✅          |\n| Node floor                                      |      ≥ 18       |      ≥ 22.18        |\n\n<sub>Compared against `mongo-migrate-kit` 1.2.2 — the version this project forked from. The Node\nfloor is a trade-off, not a win: it is what buys `.ts` migrations with no loader and `.env` parsing\nwith no dependency.</sub>\n\n→ **[Full comparison, and what stayed the same](https://migronaut.vercel.app/guide/vs-mongo-migrate-kit)**\n\n---\n\n## Quick start\n\n```bash\nnpm install @alexify/migronaut\nnpm install mongodb          # required peer dependency\n```\n\n```bash\n# 1 · create a configuration file (migronaut.config.js; pass --ts for TypeScript)\nnpx migronaut init\n\n# 2 · create your first migration\nnpx migronaut create \"add users email index\"\n\n# 3 · run everything pending\nnpx migronaut up\n\n# 4 · see where you stand\nnpx migronaut status\n```\n\nA migration is just an `up` and a `down`:\n\n```ts\nimport type { MigrationContext } from '@alexify/migronaut';\n\nexport const description = 'Add unique index on users.email';\n\nexport async function up({ db }: MigrationContext): Promise<void> {\n  await db.collection('users').createIndex({ email: 1 }, { unique: true });\n}\n\nexport async function down({ db }: MigrationContext): Promise<void> {\n  await db.collection('users').dropIndex('email_1');\n}\n```\n\n> Prefer no files at all? Skip `migronaut init` and export `MIGRONAUT_URI` and `MIGRONAUT_DB` — that is enough to run.\n\n---\n\n## Documentation\n\nFull docs, guides, and the API reference live at\n**[migronaut.vercel.app](https://migronaut.vercel.app/)**.\n\n- [Why migronaut?](https://migronaut.vercel.app/guide/why) — how it compares to `migrate-mongo`\n- [Core Concepts](https://migronaut.vercel.app/guide/concepts) — migrations, batches, the changelog, locking\n- [Getting Started](https://migronaut.vercel.app/guide/getting-started) & [Tutorial](https://migronaut.vercel.app/guide/tutorial)\n- [Configuration](https://migronaut.vercel.app/guide/configuration) · [Writing Migrations](https://migronaut.vercel.app/guide/writing-migrations) · [Transactions](https://migronaut.vercel.app/guide/transactions) · [Hooks](https://migronaut.vercel.app/guide/hooks)\n- [Programmatic API](https://migronaut.vercel.app/guide/api) · [CI/CD](https://migronaut.vercel.app/guide/ci-cd) · [Troubleshooting](https://migronaut.vercel.app/guide/troubleshooting)\n- Reference: [CLI Cheatsheet](https://migronaut.vercel.app/reference/cli) · [Error Codes](https://migronaut.vercel.app/reference/error-codes)\n\n---\n\n## Commands\n\nEvery command accepts the global flags `--uri`, `--db`, `--dir`, `--config`, `--env-file`,\n`--no-env`, `--verbose`, `--quiet`, `--no-color`, and `--json` (except `init` — see `init --format`).\n\n| Command | What it does |\n|---|---|\n| `migronaut init` | Create a documented `migronaut.config.*` in the current directory |\n| `migronaut import` | Adopt an existing `migrate-mongo` changelog (one-time, forward-only) |\n| `migronaut baseline` | Mark existing migration files as applied without running them (adopt an existing DB) |\n| `migronaut create <name>` | Generate a timestamped migration file |\n| `migronaut up [file]` | Run all pending migrations, one named file, or up to `--to <file>` |\n| `migronaut down [file]` | Roll back the last batch, a chosen batch, the last N steps, one file, or to `--to <file>` |\n| `migronaut redo [file]` | Roll back then re-apply (the last migration, or one file) |\n| `migronaut status` | Print the full migration status table (`--check` to fail CI on pending) |\n| `migronaut list` | List migrations, filtered by status |\n| `migronaut dry-run <up\\|down> [file]` | Preview a run without touching the database |\n| `migronaut audit` | Read-only health check: config, connection, transactions, indexes, lock, drift |\n| `migronaut lock` | Show who currently holds the migration lock |\n| `migronaut unlock` | Force-release a stuck lock left behind by a crashed run |\n\nMost data commands (`up`, `down`, `redo`, `status`, `list`, `dry-run`, `import`, `baseline`,\n`create`, `audit`, `lock`, `unlock`) accept **`--json`** for machine-readable output — see\n[CI & automation](#ci--automation).\n\n<details>\n<summary><b>Options for every command</b></summary>\n\n```bash\n# init — generate a config file\nmigronaut init                     # migronaut.config.js (default)\nmigronaut init --js                # migronaut.config.js (explicit default)\nmigronaut init --ts                # migronaut.config.ts\nmigronaut init --format json       # migronaut.config.json\nmigronaut init --secret-provider   # async config that loads the URI from a secret manager (js/ts only)\nmigronaut init --force             # overwrite an existing config file\nmigronaut init --uri mongodb://localhost:27017 --db my_app   # prefill the generated config\n\n# import — adopt an existing migrate-mongo changelog\nmigronaut import                   # read `changelog`, write the migronaut changelog\nmigronaut import --from <name>     # read a differently-named source collection\nmigronaut import --to <name>       # write to a specific collection (default: config migrationsCollection)\nmigronaut import --dry-run         # preview the mapping, write nothing\nmigronaut import --trust-hash      # reuse migrate-mongo's fileHash instead of recomputing\nmigronaut import --force           # proceed even if the migronaut changelog already has records\nmigronaut import --no-lock         # skip the concurrency lock (local dev only)\nmigronaut import --json            # machine-readable output\n\n# baseline — adopt an existing database (no prior migration tool)\nmigronaut baseline                 # mark ALL pending files as applied, without running them\nmigronaut baseline --to <file>     # only files up to and including <file>\nmigronaut baseline --yes           # skip the confirmation prompt (required with --json)\nmigronaut baseline --json          # machine-readable output ({ \"baselined\": [...], ... })\n\n# create — generate a migration file\nmigronaut create <name>            # file type follows config `createExtension` (default .js)\nmigronaut create <name> --ts       # force a .ts file\nmigronaut create <name> --js       # force a .js file\nmigronaut create <name> --template <path>   # use a custom template\nmigronaut create <name> --json     # machine-readable output ({ \"path\": \"...\" })\n\n# up — apply migrations\nmigronaut up                       # all pending (one shared batch for the run)\nmigronaut up <file>                # one specific file\nmigronaut up --to <file>           # pending files up to and including <file>, then stop\nmigronaut up --step                # apply each file as its own batch (revert individually later)\nmigronaut up <file> --force        # re-run an ALREADY-applied file (asks for confirmation)\nmigronaut up <file> --force --yes  # confirm a re-run non-interactively (required with --json)\nmigronaut up --strict              # abort on any checksum mismatch\nmigronaut up --no-lock             # skip the concurrency lock (local dev only)\nmigronaut up --json                # machine-readable output (array of run results)\n\n# down — roll back\nmigronaut down                     # the last batch (may be several files)\nmigronaut down <file>              # one specific file\nmigronaut down --batch <n>         # a specific batch number\nmigronaut down --steps <n>         # the last N migrations, newest first, ignoring batches\nmigronaut down --to <file>         # everything applied after <file>; <file> itself stays applied\nmigronaut down --no-lock           # skip the concurrency lock (local dev only)\nmigronaut down --json              # machine-readable output (array of run results)\n\n# redo — down then up\nmigronaut redo                     # the most recently applied migration\nmigronaut redo <file>              # a specific file\nmigronaut redo --no-lock           # skip the lock (dev only)\nmigronaut redo --json              # machine-readable output (array of run results)\n\n# status — full status table\nmigronaut status                   # the full status table\nmigronaut status --check           # exit 2 if any migration is pending (CI gate)\nmigronaut status --pending         # only the pending rows\nmigronaut status --limit <n>       # only the last N rows (not combinable with --check)\nmigronaut status --json            # machine-readable output (array of status rows)\n\n# list — filtered status\nmigronaut list                     # all migrations\nmigronaut list --pending           # only pending\nmigronaut list --applied           # only applied\nmigronaut list --json              # machine-readable output (array of status rows)\n\n# dry-run — preview, never writes\nmigronaut dry-run up [file]\nmigronaut dry-run down [file]\nmigronaut dry-run down --steps <n> # preview a step rollback (the last N migrations)\nmigronaut dry-run down --batch <n> # preview reverting a specific batch\nmigronaut dry-run up --to <file>   # preview a staged rollout up to <file>\nmigronaut dry-run down --to <file> # preview reverting everything applied after <file>\nmigronaut dry-run up --json        # machine-readable output (array of status rows)\n\n# audit — read-only health check\nmigronaut audit                    # pass/warn/fail per check; exit 22 on any fail\nmigronaut audit --json             # machine-readable report\n\n# lock — inspect the current lock\nmigronaut lock                     # shows the holder (pid / host / user / since), or \"no lock\"\nmigronaut lock --json              # machine-readable output\n\n# unlock — clear a stuck lock after a crash\nmigronaut unlock                   # shows the holder, prompts y/N\nmigronaut unlock --yes             # skip the prompt (short: -y)\nmigronaut unlock --json            # machine-readable output ({ \"released\": ..., \"holder\": ... })\n```\n\n**Global flags** (available on all commands): `--uri <uri>` (override `MIGRONAUT_URI`),\n`--db <name>` (override `MIGRONAUT_DB`), `--dir <path>` (override `MIGRONAUT_MIGRATIONS_DIR`),\n`--config <path>` (explicit config file, overrides auto-discovery), `--env-file <path>` /\n`--no-env` (control `.env` loading), `--verbose` / `--quiet` (log level), `--no-color`,\n`-V, --version`, `-h, --help`. Combined short flags are not supported — write `-f -y`, not `-fy`.\n\n**`--json`** is a global flag (`migronaut --json status` and `migronaut status --json` both\nwork) and prints one JSON document to stdout — see [CI & automation](#ci--automation). The one\ncommand without JSON output is `migronaut init`, whose deliverable is the config file itself:\nuse `init --format <js|ts|json>` to pick the file format.\n\n</details>\n\n---\n\n## Advanced features\n\n<details id=\"migrating-from-migrate-mongo\">\n<summary><b>Migrating from <code>migrate-mongo</code></b> — adopt an existing changelog with <code>migronaut import</code></summary>\n\n<br>\n\n`migronaut import` reads your existing `migrate-mongo` changelog and records that history in the `migronaut`\nchangelog, so `migronaut up` knows what is already applied and runs only what is new. It is a **one-time,\nforward-only** step.\n\n```bash\n# point migronaut at the same database, then:\nmigronaut import --dry-run     # preview the mapping first (writes nothing)\nmigronaut import               # adopt the history\nmigronaut up                   # apply only the migrations added since\n```\n\n**What it does**\n\n- Reads the source collection (`changelog` by default; `--from` to override) and **never modifies it** —\n  the mapped records are written to the `migronaut` changelog (your config's `migrationsCollection`,\n  `_migronaut_migrations` by default; `--to` to write to a different collection).\n- Maps `fileName → name`, `appliedAt → appliedAt`, and resolves a checksum: it reuses `migrate-mongo`'s\n  `fileHash` when it matches the file on disk, otherwise recomputes a SHA-256 from disk (`--trust-hash`\n  reuses the stored hash as-is). Records whose files are missing are still imported.\n- Assigns each migration a **unique, sequential batch number** in apply order. If the `migronaut` changelog\n  already has records, imported batches **continue after** the existing maximum (use `--force` to import\n  into a non-empty changelog).\n- Leaves migration files that exist on disk but are **not** in the source changelog **pending** — they\n  run on the next `migronaut up`, exactly as expected for newly added migrations.\n\n**Options**\n\n| Flag | Default | What it does |\n|---|---|---|\n| `--from <collection>` | `changelog` | Source collection to read (never modified). |\n| `--to <collection>` | config `migrationsCollection` (`_migronaut_migrations`) | Target collection to write the adopted history to. |\n| `--dry-run` | off | Preview the mapping and print the table; writes nothing. |\n| `--trust-hash` | off | Reuse `migrate-mongo`'s stored `fileHash` as-is instead of recomputing the checksum from disk. |\n| `--force` | off | Import into a changelog that already has records (imported batches continue after the existing max). |\n| `--no-lock` | off | Skip the MongoDB concurrency lock (local dev only). |\n\nPlus the global flags `--uri`, `--db`, `--dir`, and `--config`.\n\n**Forward-only — imported migrations cannot be rolled back**\n\nAdopted records are tagged `origin: 'migrate-mongo'`. `migrate-mongo` files use a positional\n`up(db, client)` signature, which `migronaut` does not execute (it passes a single context object). To avoid\never corrupting your data, `migronaut down` / `migronaut redo` **refuse** an imported migration up front, before\nrunning or writing anything, and tell you why:\n\n```text\n✖ Cannot roll back 1 migrate-mongo-imported migration(s): 20260101-add-index.js\n```\n\nIf you need an old migration to be reversible under `migronaut`, re-author its file in the native format\n(named exports, single context argument — see [Migration file formats](#migration-file-formats)).\n\n</details>\n\n<details>\n<summary><b>Transactions</b> — wrap a migration in an all-or-nothing MongoDB transaction</summary>\n\n<br>\n\nOpt in per file with `export const useTransaction = true` (or globally via config). The runner opens a\nsession, passes it through the context, and commits on success or aborts on any error. Pass the\n`session` to every operation so it joins the transaction:\n\n```ts\nexport const useTransaction = true;\n\nexport async function up({ db, session }: MigrationContext): Promise<void> {\n  await db.collection('accounts').insertOne({ balance: 100 }, { session });\n  await db.collection('ledger').insertOne({ delta: 100 }, { session });\n}\n```\n\n> Transactions require a replica set or sharded cluster — MongoDB's own requirement, not a library limit.\n\n</details>\n\n<details>\n<summary><b>Lifecycle hooks</b> — run code around the batch and each migration</summary>\n\n<br>\n\nDefine hooks in your config file. Use them to seed data, emit metrics, or alert on failure:\n\n```ts\nhooks: {\n  beforeAll:  async (ctx) => { /* once, before the batch */ },\n  afterAll:   async (ctx) => { /* once, after the batch */ },\n  beforeEach: async (name, ctx) => { /* before each file */ },\n  afterEach:  async (name, durationMs, ctx) => { /* after each file */ },\n  onError:    async (name, error, ctx) => { /* a file threw — alert, then it re-throws */ },\n}\n```\n\n</details>\n\n<details>\n<summary><b>Loading secrets at runtime</b> — AWS, Google, Vault, Azure, anything</summary>\n\n<br>\n\nA `.ts`/`.js` config may export a **function** (sync or async) instead of an object.\n`migronaut` calls it once per command, so you can fetch the connection from a secret manager at run time.\nThe secret is **never written to disk**, and a rotated value is picked up automatically on the next run.\n\nThe library ships **no** cloud SDKs — you bring the one you already use, so any provider works:\n\n```js\n// migronaut.config.js — AWS Secrets Manager\nimport { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';\n\nexport default async () => {\n  const sm = new SecretsManagerClient({ region: 'us-east-1' });\n  const res = await sm.send(new GetSecretValueCommand({ SecretId: 'prod/mongo' }));\n  const { uri, dbName } = JSON.parse(res.SecretString ?? '{}');\n  return { uri, dbName };  // merged at the config-file tier — env vars / flags still override\n};\n```\n\nRun `migronaut init --secret-provider` to scaffold this form with an AWS example you can swap for any provider.\nIf the function throws, it surfaces as a `ConfigInvalidError` with the cause attached.\n\n</details>\n\n<details>\n<summary><b>Batches &amp; step rollback</b> — group a deploy, or revert file-by-file</summary>\n\n<br>\n\nA **batch** is one `migronaut up` run. By default every migration applied in a single run shares one batch\nnumber, so `migronaut down` rolls back that whole run as a unit — the same model used by **Laravel** and\n**Knex**. That keeps a deploy atomic: one command applied it, one command reverts it.\n\nWhen you want finer control, two flags mirror Laravel's `migrate --step` / `migrate:rollback --step`:\n\n- **`migronaut up --step`** — apply each file in the run as its **own** sequential batch instead of one shared\n  batch. A later `migronaut down` then peels them off one at a time.\n- **`migronaut down --steps <n>`** — revert the **last N applied migrations**, newest first, counted as\n  individual files **regardless of batch**. `migronaut down --steps 1` reverts just the single most-recently\n  applied migration; a larger N can cross batch boundaries, so preview it first with\n  `migronaut dry-run down --steps <n>`.\n\n`--steps` is mutually exclusive with `--batch` and a filename. Migrations are always reverted\nnewest-first, so `up` followed by `down --steps <same n>` returns you to the starting state.\n\n</details>\n\n<details>\n<summary><b>Concurrency lock &amp; checksums</b> — safe concurrent deploys, tamper detection</summary>\n\n<br>\n\n**Lock.** Each run acquires an atomic lock document in `_migronaut_locks`, so two deploys can never migrate\nat once. A lock older than `lockTTLSeconds` is treated as stale and reclaimed; while a migration runs,\na heartbeat renews the lock at half the TTL so a long migration can't have its lock stolen mid-run.\nThe lock is always released in a `finally` block. `--no-lock` bypasses it for local development (and\nwarns loudly). If a process crashes hard and leaves a lock behind, clear it with **`migronaut unlock`** (it\nshows you who held it and asks for confirmation).\n\n**Checksums.** Every applied migration stores a SHA-256 of its file. On later runs `migronaut` compares the\ntwo and surfaces drift in `status`. With `strict: true` (or `--strict`) a mismatch aborts the run;\notherwise it warns and skips. To intentionally re-run an edited, already-applied file, use\n`migronaut up <file> --force`.\n\n</details>\n\n<details id=\"ci--automation\">\n<summary><b>CI &amp; automation</b> — JSON output, deploy gates, scripting</summary>\n\n<br>\n\n**Machine-readable output.** Add `--json` to any data command (`up`, `down`, `redo`, `status`,\n`list`, `dry-run`, `import`, `create`, `audit`, `lock`, `unlock`) to get a single JSON document on **stdout** — all\nhuman logs and the spinner are redirected to stderr, so the stream is safe to pipe into `jq` or parse\nin a script. On failure the command prints `{ \"error\": { \"code\": \"...\", \"message\": \"...\" } }` to\nstdout and exits `1`.\n\n```bash\n# Apply pending migrations and capture the result in CI\nmigronaut up --json | jq '.[] | select(.status == \"applied\") | .file'\n\n# Fail a deploy step if the database isn't fully migrated\nmigronaut status --check          # exits 2 when anything is pending, 0 otherwise\n\n# Inspect status as data\nmigronaut status --json | jq 'map(select(.status == \"pending\")) | length'\n```\n\nA typical pipeline gate:\n\n```yaml\n# .github/workflows/deploy.yml (excerpt)\n- name: Fail if migrations are pending\n  run: npx migronaut status --check --uri \"$MIGRONAUT_URI\" --db \"$MIGRONAUT_DB\"\n```\n\n> Note: `migronaut init` is the one command without JSON output — its deliverable is the\n> config file itself. `init --format json` writes `migronaut.config.json`.\n\n</details>\n\n<details>\n<summary><b>Audit trail</b> — a complete, append-only history</summary>\n\n<br>\n\nEvery record in `_migronaut_migrations` stores `batch`, `status`, `appliedAt`, `revertedAt`, `duration`,\n`checksum`, `environment`, and `executedBy`. Rolling back **updates** a record's status to `reverted`\nand stamps `revertedAt` — it is **never deleted**, so the full history stays intact for compliance.\n\n</details>\n\n<details>\n<summary><b>Programmatic API</b> — run migrations from your own code</summary>\n\n<br>\n\n#### Run pending migrations on app start\n\n`runMigrations()` is the blessed one-call entry point. It opens its own connection, applies every\npending migration, and **always disconnects** — even if a migration throws, so a failed boot never\nleaks a MongoDB connection. A failing migration aborts startup instead of letting your app serve\ntraffic against a half-migrated database:\n\n```ts\nimport { runMigrations } from '@alexify/migronaut';\n\n// Call this before your server starts listening.\nconst { applied, upToDate } = await runMigrations({\n  uri: process.env.MIGRONAUT_URI!,\n  dbName: 'my_app',\n  migrationsDir: './migrations',\n});\n\nif (!upToDate) console.log(`Applied ${applied.length} migration(s)`);\n// then: app.listen(...)\n```\n\n#### Multiple instances booting together\n\nWhen several instances start at once, only one wins the lock. Set `onLockHeld: 'wait'` so the others\nblock until the migrating peer finishes, then confirm there's nothing left to apply before returning:\n\n```ts\nawait runMigrations(\n  { uri: process.env.MIGRONAUT_URI!, dbName: 'my_app' },\n  { onLockHeld: 'wait', lockWaitTimeoutMs: 90_000 }, // default 'throw', waits up to 90s\n);\n```\n\n#### Serverless / cold start\n\nThe same call works in a Lambda/Cloud Function bootstrap. Keep `onLockHeld: 'wait'` so concurrent\ncold starts don't fail, and rely on the auto-disconnect so each invocation cleans up after itself.\n\n#### Fail a deploy/health check when the DB is behind (no writes)\n\n`pendingMigrations()` is a connection-managed, read-only readiness probe:\n\n```ts\nimport { pendingMigrations } from '@alexify/migronaut';\n\nconst pending = await pendingMigrations({ uri, dbName: 'my_app' });\nif (pending.length > 0) {\n  throw new Error(`Database is behind by ${pending.length} migration(s)`);\n}\n```\n\n#### Full control\n\nFor everything else, every CLI command is a method on `MigratorKit` (you manage the lifecycle):\n\n```ts\nimport { MigratorKit } from '@alexify/migronaut';\n\nconst migrator = new MigratorKit({ uri, dbName: 'my_app', migrationsDir: './migrations' });\nawait migrator.connect();\nconst rows = await migrator.status();   // StatusRow[]\nawait migrator.disconnect();\n```\n\nAll errors extend `MigronautError` and carry a typed `code` (`LOCK_ALREADY_HELD`, `CHECKSUM_MISMATCH`,\n`NOT_APPLIED`, …), so `catch` blocks stay type-safe.\n\n> [!NOTE]\n> Running several kits against several databases in **one process**? Pass `envFile: false` and\n> explicit `uri`/`dbName` to each — `.env` loading mutates the shared `process.env` (dotenv\n> semantics), so different env files could otherwise leak `MIGRONAUT_*` values between kits.\n\n</details>\n\n---\n\n## Configuration\n\n`migronaut` resolves settings in this order (**highest wins**):\n\n> **CLI flags → environment variables → config file → built-in defaults**\n\nA config file is optional and auto-discovered in the working directory as `migronaut.config.ts`,\n`migronaut.config.js`, or `migronaut.config.json`. Run `migronaut init` to generate one — it ships fully commented,\nso every setting lives in one documented place. A generated `migronaut.config.json` points its\n`$schema` at the hosted copy for editor completion; offline or air-gapped setups can point it at\nthe copy every install already ships: `./node_modules/@alexify/migronaut/migronaut.schema.json`.\n\n```js\n// migronaut.config.js — generated by `migronaut init`, every option explained\n/** @type {import('@alexify/migronaut').MigronautConfig} */\nexport default {\n  // ── Connection (required) ───────────────────────────────────────────────\n  uri: 'mongodb://localhost:27017', // MongoDB connection string\n  dbName: 'my_app',                 // database to run migrations against\n\n  // ── Files ───────────────────────────────────────────────────────────────\n  migrationsDir: './migrations',    // where migration files live\n  fileExtensions: ['.ts', '.js'],   // which files count as migrations\n  createExtension: 'js',            // default type for `migronaut create` ('js' | 'ts'); --js/--ts override\n  sequential: false,                // true → 0001-style numbering instead of timestamps\n  // templatePath: './migration.template.ts', // custom template for `migronaut create`\n\n  // ── Bookkeeping collections ─────────────────────────────────────────────\n  migrationsCollection: '_migronaut_migrations', // the append-only audit trail\n  lockCollection: '_migronaut_locks',            // the concurrency lock\n  lockTTLSeconds: 60,                       // a lock older than this is reclaimable\n\n  // ── Safety ──────────────────────────────────────────────────────────────\n  strict: false,        // true → abort on a checksum mismatch (instead of warn + skip)\n  useTransaction: false, // true → wrap every migration in a transaction (override per file)\n\n  // ── Code-only options (omit in migronaut.config.json) ─────────────────────────\n  // hooks: { beforeAll, afterAll, beforeEach, afterEach, onError },\n  // mongoose: myMongooseInstance, // pass if your migrations use Mongoose models\n  // logger: null,                 // null silences all output; a pino instance works directly\n};\n```\n\n<details>\n<summary><b>Structured logging with pino</b> — the logger option is pino-compatible</summary>\n\n<br>\n\nAnything with `{ debug, info, warn, error }` methods works as `logger` — including a real pino\ninstance. When the logger has a pino-style `child()`, migronaut binds `component: 'migronaut'`\nonce, and a throwing logger can never break a migration run:\n\n```js\nconst pino = require('pino');\nconst { runMigrations } = require('@alexify/migronaut');\n\nawait runMigrations({\n  uri: process.env.MIGRONAUT_URI,\n  dbName: 'my_app',\n  logger: pino({ level: 'info' }),\n});\n```\n\n</details>\n\n<details>\n<summary><b>Environment variables</b> — the zero-file way to configure everything</summary>\n\n<br>\n\nEvery **scalar** config option has an environment variable, which is what makes a config file\noptional rather than merely discouraged:\n\n| Env var | Config key | Default |\n|---|---|---|\n| `MIGRONAUT_URI` | `uri` | — *(required)* |\n| `MIGRONAUT_DB` | `dbName` | — *(required)* |\n| `MIGRONAUT_MIGRATIONS_DIR` | `migrationsDir` | `./migrations` |\n| `MIGRONAUT_COLLECTION` | `migrationsCollection` | `_migronaut_migrations` |\n| `MIGRONAUT_LOCK_COLLECTION` | `lockCollection` | `_migronaut_locks` |\n| `MIGRONAUT_LOCK_TTL` | `lockTTLSeconds` | `60` |\n| `MIGRONAUT_STRICT` | `strict` | `false` |\n| `MIGRONAUT_USE_TRANSACTION` | `useTransaction` | `false` |\n| `MIGRONAUT_SEQUENTIAL` | `sequential` | `false` |\n| `MIGRONAUT_CREATE_EXTENSION` | `createExtension` | `js` |\n| `MIGRONAUT_ENVIRONMENT` | `environment` | `NODE_ENV`, then `production` |\n| `MIGRONAUT_TEMPLATE_PATH` | `templatePath` | — *(built-in template)* |\n| `MIGRONAUT_TIMEOUT_MS` | `timeoutMs` | — *(no timeout)* |\n| `MIGRONAUT_ON_LOCK_LOST` | `onLockLost` | `abort` |\n| `MIGRONAUT_ON_OUT_OF_ORDER` | `onOutOfOrder` | `warn` |\n| `MIGRONAUT_ENSURE_INDEXES` | `ensureIndexes` | `true` |\n| `MIGRONAUT_RELOAD_MIGRATIONS` | `reloadMigrations` | `false` |\n| `MIGRONAUT_ENV_FILE` | `envFile` | `.env` |\n\n`fileExtensions`, `clientOptions`, `client`, `mongoose`, `hooks` and `logger` are config-file/API\nonly — they aren't scalars, so no environment variable can express them.\n\nA value that doesn't parse is **rejected, never coerced**: `MIGRONAUT_STRICT=on` or\n`MIGRONAUT_LOCK_TTL=abc` fails with an error naming the variable, rather than quietly turning a\nsafety setting off.\n\nThree more variables shape the CLI rather than the config:\n\n| Env var | Effect |\n|---|---|\n| `MIGRONAUT_NO_COLOR` | Disable ANSI color for migronaut only; outranks `NO_COLOR`/`FORCE_COLOR` |\n| `MIGRONAUT_FORCE_COLOR` | Force color on (`0` forces it off); the highest-priority color signal |\n| `MIGRONAUT_USER` | Who to record in `executedBy`; overrides the OS user (useful in CI) |\n\nThe unprefixed `NO_COLOR`, `FORCE_COLOR` and `TERM=dumb` are still honored underneath the prefixed\npair — they're ecosystem-wide conventions, and a CLI is expected to obey them.\n\n`.env` files are loaded automatically, parsed by Node's built-in `util.parseEnv`. Real environment\nvariables always win over `.env` values. Supported syntax: one `KEY=VALUE` per line, optional\n`export ` prefix, matching single/double/back quotes, full-line and inline `#` comments, and\nmultiline values inside double quotes. Not supported: `${VAR}` interpolation. Files over 1 MB are\nrejected, as is anything that isn't a regular file.\n\n</details>\n\n---\n\n## Migration file formats\n\n`migronaut` loads TypeScript and both JavaScript module systems with no extra setup:\n\n```ts\n// TypeScript / ESM — named exports (native on Node 22.18+, or under a loader like tsx)\nexport async function up({ db }) { /* ... */ }\nexport async function down({ db }) { /* ... */ }\n```\n\n```js\n// CommonJS — default export\nmodule.exports = {\n  async up({ db }) { /* ... */ },\n  async down({ db }) { /* ... */ },\n};\n```\n\nOptional per-file exports: `description` (shown in `status`) and `useTransaction`. Note that `up`/`down`\nreceive a **single context object** (`{ db, client, mongoose?, session? }`) — not `migrate-mongo`'s\npositional `(db, client)`.\n\n> **ESM vs CommonJS:** Node decides a file's module system from its extension and the nearest\n> `package.json` `\"type\"`. In a project with `\"type\": \"module\"`, a `.js` file is an ES module, so\n> `module.exports = …` throws *\"module is not defined in ES module scope.\"* Use named `export`s (above),\n> or name the file `.cjs` and add `'.cjs'` to `fileExtensions` in your config.\n\n---\n\n## Benchmarks\n\nMeasured with the zero-dependency harness in [`bench/bench.js`](./bench/bench.js) (in-process\nscenarios: 1s timed run after 2k warmup iterations; DB-bound scenarios against an in-memory\nMongoDB replica set: 500ms timed run after 100 warmup iterations). Reproduce with:\n\n```bash\npnpm bench\n```\n\nApple M3 Max, Node v24.14.1:\n\n| Scenario | ops/sec |\n|---|--------:|\n| `computeChecksum` — small migration file (~1 KB) | ~16,000 |\n| `computeChecksum` — large migration file (~100 KB) |  ~8,000 |\n| `loadMigrationFile` — CommonJS default export | ~60,000 |\n| `loadMigrationFile` — ESM named exports | ~60,000 |\n| `loadMigrationFile` — TypeScript (native type-stripping) | ~60,000 |\n| `Changelog.getAll` — 1,000 records |     ~20 |\n| `Changelog.getAppliedNames` — 1,000 records |     ~30 |\n| `Changelog.getLastBatch` — 1,000 records |    ~200 |\n| `Changelog.markApplied` — idempotent upsert |    ~150 |\n| `Changelog.markReverted` — update existing applied record |    ~150 |\n| `MigrationLock.acquire + release` — uncontended round trip |     ~80 |\n| `MigrationLock.renew` — heartbeat update |    ~150 |\n\nNumbers vary by hardware and Node version — treat them as relative guidance, not absolutes. The\nharness exists primarily to catch performance regressions between releases.\n\n---\n\n## License\n\n[MIT](./LICENSE) © Alexis Technologies\n\nOriginally forked from `mongo-migrate-kit` by Santosh Gupta; MIT, attribution\nretained in [LICENSE](./LICENSE). See\n[what changed since the fork](https://migronaut.vercel.app/guide/vs-mongo-migrate-kit).\n","readmeFilename":"README.md"}