@alexify/migronaut 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/LICENSE +22 -0
  3. package/README.md +773 -0
  4. package/bin/migronaut.js +36 -0
  5. package/index.d.ts +903 -0
  6. package/index.js +1 -0
  7. package/migronaut.schema.json +135 -0
  8. package/package.json +105 -0
  9. package/src/cli/args.js +314 -0
  10. package/src/cli/commands/audit.js +40 -0
  11. package/src/cli/commands/create.js +29 -0
  12. package/src/cli/commands/down.js +30 -0
  13. package/src/cli/commands/dry-run.js +36 -0
  14. package/src/cli/commands/import.js +33 -0
  15. package/src/cli/commands/init.js +64 -0
  16. package/src/cli/commands/list.js +19 -0
  17. package/src/cli/commands/lock.js +35 -0
  18. package/src/cli/commands/redo.js +16 -0
  19. package/src/cli/commands/status.js +52 -0
  20. package/src/cli/commands/unlock.js +43 -0
  21. package/src/cli/commands/up.js +53 -0
  22. package/src/cli/exit-codes.js +38 -0
  23. package/src/cli/index.js +91 -0
  24. package/src/cli/shared.js +343 -0
  25. package/src/cli/spinner.js +59 -0
  26. package/src/cli/table.js +259 -0
  27. package/src/core/audit.js +152 -0
  28. package/src/core/changelog.js +231 -0
  29. package/src/core/config.js +479 -0
  30. package/src/core/context.js +16 -0
  31. package/src/core/import-runner.js +164 -0
  32. package/src/core/import.js +60 -0
  33. package/src/core/lock.js +348 -0
  34. package/src/core/migrator.js +1475 -0
  35. package/src/core/run.js +144 -0
  36. package/src/core/runner.js +150 -0
  37. package/src/errors/index.js +215 -0
  38. package/src/index.js +59 -0
  39. package/src/utils/checksum.js +23 -0
  40. package/src/utils/colors.js +68 -0
  41. package/src/utils/concurrency.js +32 -0
  42. package/src/utils/date.js +28 -0
  43. package/src/utils/env.js +51 -0
  44. package/src/utils/error.js +11 -0
  45. package/src/utils/loader.js +131 -0
  46. package/src/utils/logger.js +109 -0
  47. package/src/utils/redact.js +39 -0
  48. package/src/utils/sanitize.js +43 -0
  49. package/src/utils/template.js +615 -0
  50. package/src/utils/user.js +25 -0
package/README.md ADDED
@@ -0,0 +1,773 @@
1
+ <div align="center">
2
+
3
+ <img src="https://raw.githubusercontent.com/Alexis-Technologies/migronaut/main/assets/logo-icon-b-alt2.png" alt="@alexify/migronaut" width="420" />
4
+
5
+ # migronaut
6
+
7
+ **Elegant, fast, fully-typed, zero-dependency MongoDB migrations for Node.js.**
8
+
9
+ _Adopt an existing `migrate-mongo` changelog in one command — then get the controls it never had._
10
+
11
+ [![npm](https://img.shields.io/npm/v/%40alexify%2Fmigronaut)](https://www.npmjs.com/package/@alexify/migronaut)
12
+ [![CI](https://github.com/Alexis-Technologies/migronaut/actions/workflows/ci.yml/badge.svg)](https://github.com/Alexis-Technologies/migronaut/actions/workflows/ci.yml)
13
+ [![node](https://img.shields.io/node/v/%40alexify%2Fmigronaut)](#quick-start)
14
+ [![dependencies](https://img.shields.io/badge/runtime_dependencies-0-brightgreen)](#reasons-to-choose-it)
15
+ [![docs](https://img.shields.io/badge/docs-online-blue)](https://migronaut.vercel.app/)
16
+ [![license](https://img.shields.io/npm/l/%40alexify%2Fmigronaut)](./LICENSE)
17
+
18
+
19
+ Precise, safe migrations for MongoDB. Run a single file, roll back anything, and preview every
20
+ change before it touches your database.
21
+
22
+ ### 📖 [Read the documentation →](https://migronaut.vercel.app/)
23
+
24
+ </div>
25
+
26
+ ---
27
+
28
+ ## Reasons to choose it
29
+
30
+ - **Zero dependencies** — no runtime dependencies at all; only the `mongodb` driver as a peer.
31
+ Instant installs, nothing extra in your lockfile, no supply-chain surface.
32
+ - **Run a single migration** — `migronaut up <file>`, not just "all pending".
33
+ - **Roll back anything** — a batch (`--batch 3`), the last N (`--steps 2`), one file, or `redo`.
34
+ - **Preview before you run** — `migronaut dry-run up` prints the exact plan without touching the database.
35
+ - **No race conditions** — an atomic MongoDB lock stops two deploys running migrations at once.
36
+ - **Tamper detection** — SHA-256 checksums catch a migration edited after it was applied.
37
+ - **Audit trail kept** — a rollback updates the record, it never deletes it.
38
+ - **Lifecycle hooks** — `beforeAll`, `afterAll`, `beforeEach`, `afterEach`, `onError`.
39
+ - **Opt-in transactions** — wrap a migration so it fully commits or fully aborts.
40
+ - **TypeScript, ESM & CommonJS** — all run with no `ts-node` plumbing.
41
+ - **Zero config files required** — drive everything from env vars if you prefer.
42
+ - **Pino-friendly logging** — the `logger` option is pino-compatible; pass a pino instance directly
43
+ and migronaut logs through it (with a `component: 'migronaut'` child binding).
44
+
45
+ ### How it compares to `migrate-mongo`
46
+
47
+ | Capability | `migrate-mongo` | `migronaut` |
48
+ | ----------------------------------------------- | :-------------: | :-----------------: |
49
+ | `up` / `down` / `create` / `status` | ✅ | ✅ |
50
+ | Run a single migration file | ❌ | ✅ |
51
+ | Roll back a specific batch (not just the last) | ❌ | ✅ |
52
+ | Dry-run preview | ❌ | ✅ |
53
+ | `redo` (down + up) | ❌ | ✅ |
54
+ | SHA-256 checksum / tamper detection | ❌ | ✅ |
55
+ | Lifecycle hooks | ❌ | ✅ |
56
+ | First-class TypeScript (built-in) | ❌ | ✅ |
57
+ | History preserved on rollback (never deleted) | ❌ | ✅ |
58
+ | Adopt an existing `migrate-mongo` changelog | — | ✅ `migronaut import` |
59
+
60
+ <sub>Reflects `migrate-mongo`'s documented CLI as of mid-2026. It has since added transaction access
61
+ via a `client` argument; `migronaut` exposes the same plus a declarative per-file `useTransaction` flag.</sub>
62
+
63
+ ### How it compares to `mongo-migrate-kit`
64
+
65
+ `migronaut` is a fork of [`mongo-migrate-kit`](https://www.npmjs.com/package/mongo-migrate-kit)
66
+ (CLI `mmk`) by Santosh Gupta. The fork was not a rename — everything below landed after it:
67
+
68
+ | Capability | `mongo-migrate-kit` | `migronaut` |
69
+ | ----------------------------------------------- | :-------------: | :-----------------: |
70
+ | Runtime dependencies | 6 | **0** |
71
+ | Ships as | bundled `dist/` | source, no build step |
72
+ | `--json` on every command | ❌ | ✅ |
73
+ | Typed exit code per error (`EXIT_CODES`) | ❌ | ✅ |
74
+ | `status --check` deploy gate | ❌ | ✅ |
75
+ | `audit` / `lock` commands | ❌ | ✅ |
76
+ | `up --to` / `down --to` targeting | ❌ | ✅ |
77
+ | Lifecycle events (`EventEmitter`) | ❌ | ✅ |
78
+ | Reuse an already-connected `MongoClient` | ❌ | ✅ |
79
+ | Changelog written inside the migration's transaction | ❌ | ✅ |
80
+ | Credentials masked in errors, logs and `--json` | ❌ | ✅ |
81
+ | Pino-compatible logger | ❌ | ✅ |
82
+ | Node floor | ≥ 18 | ≥ 22.18 |
83
+
84
+ <sub>Compared against `mongo-migrate-kit` 1.2.2 — the version this project forked from. The Node
85
+ floor is a trade-off, not a win: it is what buys `.ts` migrations with no loader and `.env` parsing
86
+ with no dependency.</sub>
87
+
88
+ → **[Full comparison, and what stayed the same](https://migronaut.vercel.app/guide/vs-mongo-migrate-kit)**
89
+
90
+ > [!TIP]
91
+ > ### 🔄 Already using `migrate-mongo`? Switch in under a minute.
92
+ >
93
+ > `migronaut` adopts your existing `changelog` **as-is** — no re-running migrations, no data loss, no rewriting
94
+ > files. Point it at the same database and bring your whole history over in one command:
95
+ >
96
+ > ```bash
97
+ > migronaut import # one-time: adopt your migrate-mongo changelog (it's never modified)
98
+ > migronaut up # applies only what's new — your past migrations are recognized as already applied
99
+ > ```
100
+ >
101
+ > Your applied history is preserved and new migrations run normally. Your `up`/`down`/`create`/`status`
102
+ > mental model carries over 1:1 — you just gain dry-runs, single-file control, real rollbacks, hooks,
103
+ > and locking. → **[See how it works](#advanced-features)**
104
+
105
+ ---
106
+
107
+ ## Quick start
108
+
109
+ ```bash
110
+ npm install @alexify/migronaut
111
+ npm install mongodb # required peer dependency
112
+ ```
113
+
114
+ ```bash
115
+ # 1 · create a configuration file (migronaut.config.js; pass --ts for TypeScript)
116
+ npx migronaut init
117
+
118
+ # 2 · create your first migration
119
+ npx migronaut create "add users email index"
120
+
121
+ # 3 · run everything pending
122
+ npx migronaut up
123
+
124
+ # 4 · see where you stand
125
+ npx migronaut status
126
+ ```
127
+
128
+ A migration is just an `up` and a `down`:
129
+
130
+ ```ts
131
+ import type { MigrationContext } from '@alexify/migronaut';
132
+
133
+ export const description = 'Add unique index on users.email';
134
+
135
+ export async function up({ db }: MigrationContext): Promise<void> {
136
+ await db.collection('users').createIndex({ email: 1 }, { unique: true });
137
+ }
138
+
139
+ export async function down({ db }: MigrationContext): Promise<void> {
140
+ await db.collection('users').dropIndex('email_1');
141
+ }
142
+ ```
143
+
144
+ > Prefer no files at all? Skip `migronaut init` and export `MIGRONAUT_URI` and `MIGRONAUT_DB` — that is enough to run.
145
+
146
+ ---
147
+
148
+ ## Documentation
149
+
150
+ Full docs, guides, and the API reference live at
151
+ **[migronaut.vercel.app](https://migronaut.vercel.app/)**.
152
+
153
+ - [Why migronaut?](https://migronaut.vercel.app/guide/why) — how it compares to `migrate-mongo`
154
+ - [Core Concepts](https://migronaut.vercel.app/guide/concepts) — migrations, batches, the changelog, locking
155
+ - [Getting Started](https://migronaut.vercel.app/guide/getting-started) & [Tutorial](https://migronaut.vercel.app/guide/tutorial)
156
+ - [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)
157
+ - [Programmatic API](https://migronaut.vercel.app/guide/api) · [CI/CD](https://migronaut.vercel.app/guide/ci-cd) · [Troubleshooting](https://migronaut.vercel.app/guide/troubleshooting)
158
+ - Reference: [CLI Cheatsheet](https://migronaut.vercel.app/reference/cli) · [Error Codes](https://migronaut.vercel.app/reference/error-codes)
159
+
160
+ ---
161
+
162
+ ## Commands
163
+
164
+ Every command accepts the global flags `--uri`, `--db`, `--dir`, `--config`, `--env-file`,
165
+ `--no-env`, `--verbose`, `--quiet`, `--no-color`, and `--json` (except `init` — see `init --format`).
166
+
167
+ | Command | What it does |
168
+ |---|---|
169
+ | `migronaut init` | Create a documented `migronaut.config.*` in the current directory |
170
+ | `migronaut import` | Adopt an existing `migrate-mongo` changelog (one-time, forward-only) |
171
+ | `migronaut create <name>` | Generate a timestamped migration file |
172
+ | `migronaut up [file]` | Run all pending migrations, one named file, or up to `--to <file>` |
173
+ | `migronaut down [file]` | Roll back the last batch, a chosen batch, the last N steps, one file, or to `--to <file>` |
174
+ | `migronaut redo [file]` | Roll back then re-apply (the last migration, or one file) |
175
+ | `migronaut status` | Print the full migration status table (`--check` to fail CI on pending) |
176
+ | `migronaut list` | List migrations, filtered by status |
177
+ | `migronaut dry-run <up\|down> [file]` | Preview a run without touching the database |
178
+ | `migronaut audit` | Read-only health check: config, connection, transactions, indexes, lock, drift |
179
+ | `migronaut lock` | Show who currently holds the migration lock |
180
+ | `migronaut unlock` | Force-release a stuck lock left behind by a crashed run |
181
+
182
+ Most data commands (`up`, `down`, `redo`, `status`, `list`, `dry-run`, `import`, `create`,
183
+ `audit`, `lock`, `unlock`) accept **`--json`** for machine-readable output — see
184
+ [CI & automation](#ci--automation).
185
+
186
+ <details>
187
+ <summary><b>Options for every command</b></summary>
188
+
189
+ ```bash
190
+ # init — generate a config file
191
+ migronaut init # migronaut.config.js (default)
192
+ migronaut init --js # migronaut.config.js (explicit default)
193
+ migronaut init --ts # migronaut.config.ts
194
+ migronaut init --format json # migronaut.config.json
195
+ migronaut init --secret-provider # async config that loads the URI from a secret manager (js/ts only)
196
+ migronaut init --force # overwrite an existing config file
197
+ migronaut init --uri mongodb://localhost:27017 --db my_app # prefill the generated config
198
+
199
+ # import — adopt an existing migrate-mongo changelog
200
+ migronaut import # read `changelog`, write the migronaut changelog
201
+ migronaut import --from <name> # read a differently-named source collection
202
+ migronaut import --to <name> # write to a specific collection (default: config migrationsCollection)
203
+ migronaut import --dry-run # preview the mapping, write nothing
204
+ migronaut import --trust-hash # reuse migrate-mongo's fileHash instead of recomputing
205
+ migronaut import --force # proceed even if the migronaut changelog already has records
206
+ migronaut import --no-lock # skip the concurrency lock (local dev only)
207
+ migronaut import --json # machine-readable output
208
+
209
+ # create — generate a migration file
210
+ migronaut create <name> # file type follows config `createExtension` (default .js)
211
+ migronaut create <name> --ts # force a .ts file
212
+ migronaut create <name> --js # force a .js file
213
+ migronaut create <name> --template <path> # use a custom template
214
+ migronaut create <name> --json # machine-readable output ({ "path": "..." })
215
+
216
+ # up — apply migrations
217
+ migronaut up # all pending (one shared batch for the run)
218
+ migronaut up <file> # one specific file
219
+ migronaut up --to <file> # pending files up to and including <file>, then stop
220
+ migronaut up --step # apply each file as its own batch (revert individually later)
221
+ migronaut up <file> --force # re-run an ALREADY-applied file (asks for confirmation)
222
+ migronaut up <file> --force --yes # confirm a re-run non-interactively (required with --json)
223
+ migronaut up --strict # abort on any checksum mismatch
224
+ migronaut up --no-lock # skip the concurrency lock (local dev only)
225
+ migronaut up --json # machine-readable output (array of run results)
226
+
227
+ # down — roll back
228
+ migronaut down # the last batch (may be several files)
229
+ migronaut down <file> # one specific file
230
+ migronaut down --batch <n> # a specific batch number
231
+ migronaut down --steps <n> # the last N migrations, newest first, ignoring batches
232
+ migronaut down --to <file> # everything applied after <file>; <file> itself stays applied
233
+ migronaut down --no-lock # skip the concurrency lock (local dev only)
234
+ migronaut down --json # machine-readable output (array of run results)
235
+
236
+ # redo — down then up
237
+ migronaut redo # the most recently applied migration
238
+ migronaut redo <file> # a specific file
239
+ migronaut redo --no-lock # skip the lock (dev only)
240
+ migronaut redo --json # machine-readable output (array of run results)
241
+
242
+ # status — full status table
243
+ migronaut status # the full status table
244
+ migronaut status --check # exit 2 if any migration is pending (CI gate)
245
+ migronaut status --pending # only the pending rows
246
+ migronaut status --limit <n> # only the last N rows (not combinable with --check)
247
+ migronaut status --json # machine-readable output (array of status rows)
248
+
249
+ # list — filtered status
250
+ migronaut list # all migrations
251
+ migronaut list --pending # only pending
252
+ migronaut list --applied # only applied
253
+ migronaut list --json # machine-readable output (array of status rows)
254
+
255
+ # dry-run — preview, never writes
256
+ migronaut dry-run up [file]
257
+ migronaut dry-run down [file]
258
+ migronaut dry-run down --steps <n> # preview a step rollback (the last N migrations)
259
+ migronaut dry-run down --batch <n> # preview reverting a specific batch
260
+ migronaut dry-run up --to <file> # preview a staged rollout up to <file>
261
+ migronaut dry-run down --to <file> # preview reverting everything applied after <file>
262
+ migronaut dry-run up --json # machine-readable output (array of status rows)
263
+
264
+ # audit — read-only health check
265
+ migronaut audit # pass/warn/fail per check; exit 22 on any fail
266
+ migronaut audit --json # machine-readable report
267
+
268
+ # lock — inspect the current lock
269
+ migronaut lock # shows the holder (pid / host / user / since), or "no lock"
270
+ migronaut lock --json # machine-readable output
271
+
272
+ # unlock — clear a stuck lock after a crash
273
+ migronaut unlock # shows the holder, prompts y/N
274
+ migronaut unlock --yes # skip the prompt (short: -y)
275
+ migronaut unlock --json # machine-readable output ({ "released": ..., "holder": ... })
276
+ ```
277
+
278
+ **Global flags** (available on all commands): `--uri <uri>` (override `MIGRONAUT_URI`),
279
+ `--db <name>` (override `MIGRONAUT_DB`), `--dir <path>` (override `MIGRONAUT_MIGRATIONS_DIR`),
280
+ `--config <path>` (explicit config file, overrides auto-discovery), `--env-file <path>` /
281
+ `--no-env` (control `.env` loading), `--verbose` / `--quiet` (log level), `--no-color`,
282
+ `-V, --version`, `-h, --help`. Combined short flags are not supported — write `-f -y`, not `-fy`.
283
+
284
+ **`--json`** is a global flag (`migronaut --json status` and `migronaut status --json` both
285
+ work) and prints one JSON document to stdout — see [CI & automation](#ci--automation). The one
286
+ command without JSON output is `migronaut init`, whose deliverable is the config file itself:
287
+ use `init --format <js|ts|json>` to pick the file format.
288
+
289
+ </details>
290
+
291
+ ---
292
+
293
+ ## Advanced features
294
+
295
+ <details id="migrating-from-migrate-mongo">
296
+ <summary><b>Migrating from <code>migrate-mongo</code></b> — adopt an existing changelog with <code>migronaut import</code></summary>
297
+
298
+ <br>
299
+
300
+ `migronaut import` reads your existing `migrate-mongo` changelog and records that history in the `migronaut`
301
+ changelog, so `migronaut up` knows what is already applied and runs only what is new. It is a **one-time,
302
+ forward-only** step.
303
+
304
+ ```bash
305
+ # point migronaut at the same database, then:
306
+ migronaut import --dry-run # preview the mapping first (writes nothing)
307
+ migronaut import # adopt the history
308
+ migronaut up # apply only the migrations added since
309
+ ```
310
+
311
+ **What it does**
312
+
313
+ - Reads the source collection (`changelog` by default; `--from` to override) and **never modifies it** —
314
+ the mapped records are written to the `migronaut` changelog (your config's `migrationsCollection`,
315
+ `_migronaut_migrations` by default; `--to` to write to a different collection).
316
+ - Maps `fileName → name`, `appliedAt → appliedAt`, and resolves a checksum: it reuses `migrate-mongo`'s
317
+ `fileHash` when it matches the file on disk, otherwise recomputes a SHA-256 from disk (`--trust-hash`
318
+ reuses the stored hash as-is). Records whose files are missing are still imported.
319
+ - Assigns each migration a **unique, sequential batch number** in apply order. If the `migronaut` changelog
320
+ already has records, imported batches **continue after** the existing maximum (use `--force` to import
321
+ into a non-empty changelog).
322
+ - Leaves migration files that exist on disk but are **not** in the source changelog **pending** — they
323
+ run on the next `migronaut up`, exactly as expected for newly added migrations.
324
+
325
+ **Options**
326
+
327
+ | Flag | Default | What it does |
328
+ |---|---|---|
329
+ | `--from <collection>` | `changelog` | Source collection to read (never modified). |
330
+ | `--to <collection>` | config `migrationsCollection` (`_migronaut_migrations`) | Target collection to write the adopted history to. |
331
+ | `--dry-run` | off | Preview the mapping and print the table; writes nothing. |
332
+ | `--trust-hash` | off | Reuse `migrate-mongo`'s stored `fileHash` as-is instead of recomputing the checksum from disk. |
333
+ | `--force` | off | Import into a changelog that already has records (imported batches continue after the existing max). |
334
+ | `--no-lock` | off | Skip the MongoDB concurrency lock (local dev only). |
335
+
336
+ Plus the global flags `--uri`, `--db`, `--dir`, and `--config`.
337
+
338
+ **Forward-only — imported migrations cannot be rolled back**
339
+
340
+ Adopted records are tagged `origin: 'migrate-mongo'`. `migrate-mongo` files use a positional
341
+ `up(db, client)` signature, which `migronaut` does not execute (it passes a single context object). To avoid
342
+ ever corrupting your data, `migronaut down` / `migronaut redo` **refuse** an imported migration up front, before
343
+ running or writing anything, and tell you why:
344
+
345
+ ```text
346
+ ✖ Cannot roll back 1 migrate-mongo-imported migration(s): 20260101-add-index.js
347
+ ```
348
+
349
+ If you need an old migration to be reversible under `migronaut`, re-author its file in the native format
350
+ (named exports, single context argument — see [Migration file formats](#migration-file-formats)).
351
+
352
+ </details>
353
+
354
+ <details>
355
+ <summary><b>Transactions</b> — wrap a migration in an all-or-nothing MongoDB transaction</summary>
356
+
357
+ <br>
358
+
359
+ Opt in per file with `export const useTransaction = true` (or globally via config). The runner opens a
360
+ session, passes it through the context, and commits on success or aborts on any error. Pass the
361
+ `session` to every operation so it joins the transaction:
362
+
363
+ ```ts
364
+ export const useTransaction = true;
365
+
366
+ export async function up({ db, session }: MigrationContext): Promise<void> {
367
+ await db.collection('accounts').insertOne({ balance: 100 }, { session });
368
+ await db.collection('ledger').insertOne({ delta: 100 }, { session });
369
+ }
370
+ ```
371
+
372
+ > Transactions require a replica set or sharded cluster — MongoDB's own requirement, not a library limit.
373
+
374
+ </details>
375
+
376
+ <details>
377
+ <summary><b>Lifecycle hooks</b> — run code around the batch and each migration</summary>
378
+
379
+ <br>
380
+
381
+ Define hooks in your config file. Use them to seed data, emit metrics, or alert on failure:
382
+
383
+ ```ts
384
+ hooks: {
385
+ beforeAll: async (ctx) => { /* once, before the batch */ },
386
+ afterAll: async (ctx) => { /* once, after the batch */ },
387
+ beforeEach: async (name, ctx) => { /* before each file */ },
388
+ afterEach: async (name, durationMs, ctx) => { /* after each file */ },
389
+ onError: async (name, error, ctx) => { /* a file threw — alert, then it re-throws */ },
390
+ }
391
+ ```
392
+
393
+ </details>
394
+
395
+ <details>
396
+ <summary><b>Loading secrets at runtime</b> — AWS, Google, Vault, Azure, anything</summary>
397
+
398
+ <br>
399
+
400
+ A `.ts`/`.js` config may export a **function** (sync or async) instead of an object.
401
+ `migronaut` calls it once per command, so you can fetch the connection from a secret manager at run time.
402
+ The secret is **never written to disk**, and a rotated value is picked up automatically on the next run.
403
+
404
+ The library ships **no** cloud SDKs — you bring the one you already use, so any provider works:
405
+
406
+ ```js
407
+ // migronaut.config.js — AWS Secrets Manager
408
+ import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
409
+
410
+ export default async () => {
411
+ const sm = new SecretsManagerClient({ region: 'us-east-1' });
412
+ const res = await sm.send(new GetSecretValueCommand({ SecretId: 'prod/mongo' }));
413
+ const { uri, dbName } = JSON.parse(res.SecretString ?? '{}');
414
+ return { uri, dbName }; // merged at the config-file tier — env vars / flags still override
415
+ };
416
+ ```
417
+
418
+ Run `migronaut init --secret-provider` to scaffold this form with an AWS example you can swap for any provider.
419
+ If the function throws, it surfaces as a `ConfigInvalidError` with the cause attached.
420
+
421
+ </details>
422
+
423
+ <details>
424
+ <summary><b>Batches &amp; step rollback</b> — group a deploy, or revert file-by-file</summary>
425
+
426
+ <br>
427
+
428
+ A **batch** is one `migronaut up` run. By default every migration applied in a single run shares one batch
429
+ number, so `migronaut down` rolls back that whole run as a unit — the same model used by **Laravel** and
430
+ **Knex**. That keeps a deploy atomic: one command applied it, one command reverts it.
431
+
432
+ When you want finer control, two flags mirror Laravel's `migrate --step` / `migrate:rollback --step`:
433
+
434
+ - **`migronaut up --step`** — apply each file in the run as its **own** sequential batch instead of one shared
435
+ batch. A later `migronaut down` then peels them off one at a time.
436
+ - **`migronaut down --steps <n>`** — revert the **last N applied migrations**, newest first, counted as
437
+ individual files **regardless of batch**. `migronaut down --steps 1` reverts just the single most-recently
438
+ applied migration; a larger N can cross batch boundaries, so preview it first with
439
+ `migronaut dry-run down --steps <n>`.
440
+
441
+ `--steps` is mutually exclusive with `--batch` and a filename. Migrations are always reverted
442
+ newest-first, so `up` followed by `down --steps <same n>` returns you to the starting state.
443
+
444
+ </details>
445
+
446
+ <details>
447
+ <summary><b>Concurrency lock &amp; checksums</b> — safe concurrent deploys, tamper detection</summary>
448
+
449
+ <br>
450
+
451
+ **Lock.** Each run acquires an atomic lock document in `_migronaut_locks`, so two deploys can never migrate
452
+ at once. A lock older than `lockTTLSeconds` is treated as stale and reclaimed; while a migration runs,
453
+ a heartbeat renews the lock at half the TTL so a long migration can't have its lock stolen mid-run.
454
+ The lock is always released in a `finally` block. `--no-lock` bypasses it for local development (and
455
+ warns loudly). If a process crashes hard and leaves a lock behind, clear it with **`migronaut unlock`** (it
456
+ shows you who held it and asks for confirmation).
457
+
458
+ **Checksums.** Every applied migration stores a SHA-256 of its file. On later runs `migronaut` compares the
459
+ two and surfaces drift in `status`. With `strict: true` (or `--strict`) a mismatch aborts the run;
460
+ otherwise it warns and skips. To intentionally re-run an edited, already-applied file, use
461
+ `migronaut up <file> --force`.
462
+
463
+ </details>
464
+
465
+ <details id="ci--automation">
466
+ <summary><b>CI &amp; automation</b> — JSON output, deploy gates, scripting</summary>
467
+
468
+ <br>
469
+
470
+ **Machine-readable output.** Add `--json` to any data command (`up`, `down`, `redo`, `status`,
471
+ `list`, `dry-run`, `import`, `create`, `audit`, `lock`, `unlock`) to get a single JSON document on **stdout** — all
472
+ human logs and the spinner are redirected to stderr, so the stream is safe to pipe into `jq` or parse
473
+ in a script. On failure the command prints `{ "error": { "code": "...", "message": "..." } }` to
474
+ stdout and exits `1`.
475
+
476
+ ```bash
477
+ # Apply pending migrations and capture the result in CI
478
+ migronaut up --json | jq '.[] | select(.status == "applied") | .file'
479
+
480
+ # Fail a deploy step if the database isn't fully migrated
481
+ migronaut status --check # exits 2 when anything is pending, 0 otherwise
482
+
483
+ # Inspect status as data
484
+ migronaut status --json | jq 'map(select(.status == "pending")) | length'
485
+ ```
486
+
487
+ A typical pipeline gate:
488
+
489
+ ```yaml
490
+ # .github/workflows/deploy.yml (excerpt)
491
+ - name: Fail if migrations are pending
492
+ run: npx migronaut status --check --uri "$MIGRONAUT_URI" --db "$MIGRONAUT_DB"
493
+ ```
494
+
495
+ > Note: `migronaut init` is the one command without JSON output — its deliverable is the
496
+ > config file itself. `init --format json` writes `migronaut.config.json`.
497
+
498
+ </details>
499
+
500
+ <details>
501
+ <summary><b>Audit trail</b> — a complete, append-only history</summary>
502
+
503
+ <br>
504
+
505
+ Every record in `_migronaut_migrations` stores `batch`, `status`, `appliedAt`, `revertedAt`, `duration`,
506
+ `checksum`, `environment`, and `executedBy`. Rolling back **updates** a record's status to `reverted`
507
+ and stamps `revertedAt` — it is **never deleted**, so the full history stays intact for compliance.
508
+
509
+ </details>
510
+
511
+ <details>
512
+ <summary><b>Programmatic API</b> — run migrations from your own code</summary>
513
+
514
+ <br>
515
+
516
+ #### Run pending migrations on app start
517
+
518
+ `runMigrations()` is the blessed one-call entry point. It opens its own connection, applies every
519
+ pending migration, and **always disconnects** — even if a migration throws, so a failed boot never
520
+ leaks a MongoDB connection. A failing migration aborts startup instead of letting your app serve
521
+ traffic against a half-migrated database:
522
+
523
+ ```ts
524
+ import { runMigrations } from '@alexify/migronaut';
525
+
526
+ // Call this before your server starts listening.
527
+ const { applied, upToDate } = await runMigrations({
528
+ uri: process.env.MIGRONAUT_URI!,
529
+ dbName: 'my_app',
530
+ migrationsDir: './migrations',
531
+ });
532
+
533
+ if (!upToDate) console.log(`Applied ${applied.length} migration(s)`);
534
+ // then: app.listen(...)
535
+ ```
536
+
537
+ #### Multiple instances booting together
538
+
539
+ When several instances start at once, only one wins the lock. Set `onLockHeld: 'wait'` so the others
540
+ block until the migrating peer finishes, then confirm there's nothing left to apply before returning:
541
+
542
+ ```ts
543
+ await runMigrations(
544
+ { uri: process.env.MIGRONAUT_URI!, dbName: 'my_app' },
545
+ { onLockHeld: 'wait', lockWaitTimeoutMs: 90_000 }, // default 'throw', waits up to 90s
546
+ );
547
+ ```
548
+
549
+ #### Serverless / cold start
550
+
551
+ The same call works in a Lambda/Cloud Function bootstrap. Keep `onLockHeld: 'wait'` so concurrent
552
+ cold starts don't fail, and rely on the auto-disconnect so each invocation cleans up after itself.
553
+
554
+ #### Fail a deploy/health check when the DB is behind (no writes)
555
+
556
+ `pendingMigrations()` is a connection-managed, read-only readiness probe:
557
+
558
+ ```ts
559
+ import { pendingMigrations } from '@alexify/migronaut';
560
+
561
+ const pending = await pendingMigrations({ uri, dbName: 'my_app' });
562
+ if (pending.length > 0) {
563
+ throw new Error(`Database is behind by ${pending.length} migration(s)`);
564
+ }
565
+ ```
566
+
567
+ #### Full control
568
+
569
+ For everything else, every CLI command is a method on `MigratorKit` (you manage the lifecycle):
570
+
571
+ ```ts
572
+ import { MigratorKit } from '@alexify/migronaut';
573
+
574
+ const migrator = new MigratorKit({ uri, dbName: 'my_app', migrationsDir: './migrations' });
575
+ await migrator.connect();
576
+ const rows = await migrator.status(); // StatusRow[]
577
+ await migrator.disconnect();
578
+ ```
579
+
580
+ All errors extend `MigronautError` and carry a typed `code` (`LOCK_ALREADY_HELD`, `CHECKSUM_MISMATCH`,
581
+ `NOT_APPLIED`, …), so `catch` blocks stay type-safe.
582
+
583
+ </details>
584
+
585
+ ---
586
+
587
+ ## Configuration
588
+
589
+ `migronaut` resolves settings in this order (**highest wins**):
590
+
591
+ > **CLI flags → environment variables → config file → built-in defaults**
592
+
593
+ A config file is optional and auto-discovered in the working directory as `migronaut.config.ts`,
594
+ `migronaut.config.js`, or `migronaut.config.json`. Run `migronaut init` to generate one — it ships fully commented,
595
+ so every setting lives in one documented place:
596
+
597
+ ```js
598
+ // migronaut.config.js — generated by `migronaut init`, every option explained
599
+ /** @type {import('@alexify/migronaut').MigronautConfig} */
600
+ export default {
601
+ // ── Connection (required) ───────────────────────────────────────────────
602
+ uri: 'mongodb://localhost:27017', // MongoDB connection string
603
+ dbName: 'my_app', // database to run migrations against
604
+
605
+ // ── Files ───────────────────────────────────────────────────────────────
606
+ migrationsDir: './migrations', // where migration files live
607
+ fileExtensions: ['.ts', '.js'], // which files count as migrations
608
+ createExtension: 'js', // default type for `migronaut create` ('js' | 'ts'); --js/--ts override
609
+ sequential: false, // true → 0001-style numbering instead of timestamps
610
+ // templatePath: './migration.template.ts', // custom template for `migronaut create`
611
+
612
+ // ── Bookkeeping collections ─────────────────────────────────────────────
613
+ migrationsCollection: '_migronaut_migrations', // the append-only audit trail
614
+ lockCollection: '_migronaut_locks', // the concurrency lock
615
+ lockTTLSeconds: 60, // a lock older than this is reclaimable
616
+
617
+ // ── Safety ──────────────────────────────────────────────────────────────
618
+ strict: false, // true → abort on a checksum mismatch (instead of warn + skip)
619
+ useTransaction: false, // true → wrap every migration in a transaction (override per file)
620
+
621
+ // ── Code-only options (omit in migronaut.config.json) ─────────────────────────
622
+ // hooks: { beforeAll, afterAll, beforeEach, afterEach, onError },
623
+ // mongoose: myMongooseInstance, // pass if your migrations use Mongoose models
624
+ // logger: null, // null silences all output; a pino instance works directly
625
+ };
626
+ ```
627
+
628
+ <details>
629
+ <summary><b>Structured logging with pino</b> — the logger option is pino-compatible</summary>
630
+
631
+ <br>
632
+
633
+ Anything with `{ debug, info, warn, error }` methods works as `logger` — including a real pino
634
+ instance. When the logger has a pino-style `child()`, migronaut binds `component: 'migronaut'`
635
+ once, and a throwing logger can never break a migration run:
636
+
637
+ ```js
638
+ const pino = require('pino');
639
+ const { runMigrations } = require('@alexify/migronaut');
640
+
641
+ await runMigrations({
642
+ uri: process.env.MIGRONAUT_URI,
643
+ dbName: 'my_app',
644
+ logger: pino({ level: 'info' }),
645
+ });
646
+ ```
647
+
648
+ </details>
649
+
650
+ <details>
651
+ <summary><b>Environment variables</b> — the zero-file way to configure everything</summary>
652
+
653
+ <br>
654
+
655
+ Every **scalar** config option has an environment variable, which is what makes a config file
656
+ optional rather than merely discouraged:
657
+
658
+ | Env var | Config key | Default |
659
+ |---|---|---|
660
+ | `MIGRONAUT_URI` | `uri` | — *(required)* |
661
+ | `MIGRONAUT_DB` | `dbName` | — *(required)* |
662
+ | `MIGRONAUT_MIGRATIONS_DIR` | `migrationsDir` | `./migrations` |
663
+ | `MIGRONAUT_COLLECTION` | `migrationsCollection` | `_migronaut_migrations` |
664
+ | `MIGRONAUT_LOCK_COLLECTION` | `lockCollection` | `_migronaut_locks` |
665
+ | `MIGRONAUT_LOCK_TTL` | `lockTTLSeconds` | `60` |
666
+ | `MIGRONAUT_STRICT` | `strict` | `false` |
667
+ | `MIGRONAUT_USE_TRANSACTION` | `useTransaction` | `false` |
668
+ | `MIGRONAUT_SEQUENTIAL` | `sequential` | `false` |
669
+ | `MIGRONAUT_CREATE_EXTENSION` | `createExtension` | `js` |
670
+ | `MIGRONAUT_ENVIRONMENT` | `environment` | `NODE_ENV`, then `production` |
671
+ | `MIGRONAUT_TEMPLATE_PATH` | `templatePath` | — *(built-in template)* |
672
+ | `MIGRONAUT_TIMEOUT_MS` | `timeoutMs` | — *(no timeout)* |
673
+ | `MIGRONAUT_ON_LOCK_LOST` | `onLockLost` | `abort` |
674
+ | `MIGRONAUT_ENSURE_INDEXES` | `ensureIndexes` | `true` |
675
+ | `MIGRONAUT_RELOAD_MIGRATIONS` | `reloadMigrations` | `false` |
676
+ | `MIGRONAUT_ENV_FILE` | `envFile` | `.env` |
677
+
678
+ `fileExtensions`, `clientOptions`, `client`, `mongoose`, `hooks` and `logger` are config-file/API
679
+ only — they aren't scalars, so no environment variable can express them.
680
+
681
+ A value that doesn't parse is **rejected, never coerced**: `MIGRONAUT_STRICT=on` or
682
+ `MIGRONAUT_LOCK_TTL=abc` fails with an error naming the variable, rather than quietly turning a
683
+ safety setting off.
684
+
685
+ Three more variables shape the CLI rather than the config:
686
+
687
+ | Env var | Effect |
688
+ |---|---|
689
+ | `MIGRONAUT_NO_COLOR` | Disable ANSI color for migronaut only; outranks `NO_COLOR`/`FORCE_COLOR` |
690
+ | `MIGRONAUT_FORCE_COLOR` | Force color on (`0` forces it off); the highest-priority color signal |
691
+ | `MIGRONAUT_USER` | Who to record in `executedBy`; overrides the OS user (useful in CI) |
692
+
693
+ The unprefixed `NO_COLOR`, `FORCE_COLOR` and `TERM=dumb` are still honored underneath the prefixed
694
+ pair — they're ecosystem-wide conventions, and a CLI is expected to obey them.
695
+
696
+ `.env` files are loaded automatically, parsed by Node's built-in `util.parseEnv`. Real environment
697
+ variables always win over `.env` values. Supported syntax: one `KEY=VALUE` per line, optional
698
+ `export ` prefix, matching single/double/back quotes, full-line and inline `#` comments, and
699
+ multiline values inside double quotes. Not supported: `${VAR}` interpolation. Files over 1 MB are
700
+ rejected, as is anything that isn't a regular file.
701
+
702
+ </details>
703
+
704
+ ---
705
+
706
+ ## Migration file formats
707
+
708
+ `migronaut` loads TypeScript and both JavaScript module systems with no extra setup:
709
+
710
+ ```ts
711
+ // TypeScript / ESM — named exports (native on Node 22.18+, or under a loader like tsx)
712
+ export async function up({ db }) { /* ... */ }
713
+ export async function down({ db }) { /* ... */ }
714
+ ```
715
+
716
+ ```js
717
+ // CommonJS — default export
718
+ module.exports = {
719
+ async up({ db }) { /* ... */ },
720
+ async down({ db }) { /* ... */ },
721
+ };
722
+ ```
723
+
724
+ Optional per-file exports: `description` (shown in `status`) and `useTransaction`. Note that `up`/`down`
725
+ receive a **single context object** (`{ db, client, mongoose?, session? }`) — not `migrate-mongo`'s
726
+ positional `(db, client)`.
727
+
728
+ > **ESM vs CommonJS:** Node decides a file's module system from its extension and the nearest
729
+ > `package.json` `"type"`. In a project with `"type": "module"`, a `.js` file is an ES module, so
730
+ > `module.exports = …` throws *"module is not defined in ES module scope."* Use named `export`s (above),
731
+ > or name the file `.cjs` and add `'.cjs'` to `fileExtensions` in your config.
732
+
733
+ ---
734
+
735
+ ## Benchmarks
736
+
737
+ Measured with the zero-dependency harness in [`bench/bench.js`](./bench/bench.js) (in-process
738
+ scenarios: 1s timed run after 2k warmup iterations; DB-bound scenarios against an in-memory
739
+ MongoDB replica set: 500ms timed run after 100 warmup iterations). Reproduce with:
740
+
741
+ ```bash
742
+ pnpm bench
743
+ ```
744
+
745
+ Apple M3 Max, Node v24.14.1:
746
+
747
+ | Scenario | ops/sec |
748
+ |---|--------:|
749
+ | `computeChecksum` — small migration file (~1 KB) | ~16,000 |
750
+ | `computeChecksum` — large migration file (~100 KB) | ~8,000 |
751
+ | `loadMigrationFile` — CommonJS default export | ~60,000 |
752
+ | `loadMigrationFile` — ESM named exports | ~60,000 |
753
+ | `loadMigrationFile` — TypeScript (native type-stripping) | ~60,000 |
754
+ | `Changelog.getAll` — 1,000 records | ~20 |
755
+ | `Changelog.getAppliedNames` — 1,000 records | ~30 |
756
+ | `Changelog.getLastBatch` — 1,000 records | ~200 |
757
+ | `Changelog.markApplied` — idempotent upsert | ~150 |
758
+ | `Changelog.markReverted` — update existing applied record | ~150 |
759
+ | `MigrationLock.acquire + release` — uncontended round trip | ~80 |
760
+ | `MigrationLock.renew` — heartbeat update | ~150 |
761
+
762
+ Numbers vary by hardware and Node version — treat them as relative guidance, not absolutes. The
763
+ harness exists primarily to catch performance regressions between releases.
764
+
765
+ ---
766
+
767
+ ## License
768
+
769
+ [MIT](./LICENSE) © Alexis Technologies
770
+
771
+ Originally forked from `mongo-migrate-kit` by Santosh Gupta; MIT, attribution
772
+ retained in [LICENSE](./LICENSE). See
773
+ [what changed since the fork](https://migronaut.vercel.app/guide/vs-mongo-migrate-kit).