migraguard 0.9.1 → 0.9.2

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.
package/cli-contract.yaml CHANGED
@@ -77,9 +77,16 @@ commandSets:
77
77
  type: boolean
78
78
  default: false
79
79
 
80
+ - name: dry-run
81
+ aliases: [n]
82
+ description: Show what would be created without writing files.
83
+ schema:
84
+ type: boolean
85
+ default: false
86
+
80
87
  exits:
81
88
  '0':
82
- description: Migration file (or group directory) created successfully.
89
+ description: Migration file (or group directory) created successfully (or dry-run preview completed).
83
90
  stdout:
84
91
  format: text
85
92
  files:
@@ -105,6 +112,10 @@ commandSets:
105
112
  idempotent: false
106
113
  sideEffects:
107
114
  - file_write
115
+ sideEffectNote: Creates migration SQL file(s) in the configured migrations directory.
116
+ safeDryRunOption: dry-run
117
+ expectedDurationMs: 1000
118
+ retryableExitCodes: []
108
119
 
109
120
  # ── apply ────────────────────────────────────────
110
121
  apply:
@@ -168,7 +179,12 @@ commandSets:
168
179
  requires writing a compensating migration or restoring from backup.
169
180
  sideEffects:
170
181
  - database_write
182
+ sideEffectNote: >-
183
+ Executes SQL via native CLI and records results in schema_migrations.
184
+ Uses advisory lock to prevent concurrent execution.
171
185
  safeDryRunOption: dry-run
186
+ expectedDurationMs: 60000
187
+ retryableExitCodes: []
172
188
  recommendedBeforeUse:
173
189
  - Run migraguard check to verify metadata integrity.
174
190
  - Run migraguard lint to check for unsafe DDL patterns.
@@ -192,6 +208,11 @@ commandSets:
192
208
  format: text
193
209
 
194
210
  '1':
211
+ description: Unexpected error (file read failure, parse error).
212
+ stderr:
213
+ format: text
214
+
215
+ '10':
195
216
  description: Integrity check failed (tampered, missing, or extra files).
196
217
  stderr:
197
218
  format: text
@@ -201,6 +222,9 @@ commandSets:
201
222
  requiresConfirmation: false
202
223
  idempotent: true
203
224
  sideEffects: []
225
+ sideEffectNote: Read-only. Compares metadata.json against files on disk.
226
+ expectedDurationMs: 2000
227
+ retryableExitCodes: []
204
228
 
205
229
  # ── squash ───────────────────────────────────────
206
230
  squash:
@@ -211,6 +235,15 @@ commandSets:
211
235
  recovery and hotfix workflows. Original migration files are deleted.
212
236
  usage:
213
237
  - migraguard squash
238
+ - migraguard squash --dry-run
239
+
240
+ options:
241
+ - name: dry-run
242
+ aliases: [n]
243
+ description: Show what would be squashed without writing files.
244
+ schema:
245
+ type: boolean
246
+ default: false
214
247
 
215
248
  exits:
216
249
  '0':
@@ -240,6 +273,12 @@ commandSets:
240
273
  sideEffects:
241
274
  - file_write
242
275
  - file_delete
276
+ sideEffectNote: >-
277
+ Merges migration files into one and deletes originals.
278
+ Updates metadata.json.
279
+ safeDryRunOption: dry-run
280
+ expectedDurationMs: 3000
281
+ retryableExitCodes: []
243
282
  recommendedBeforeUse:
244
283
  - Ensure all pending files are ready for release.
245
284
  - Confirm no uncommitted changes to migration files.
@@ -290,6 +329,9 @@ commandSets:
290
329
  requiresConfirmation: false
291
330
  idempotent: true
292
331
  sideEffects: []
332
+ sideEffectNote: Read-only. Scans migration files against safety rules.
333
+ expectedDurationMs: 3000
334
+ retryableExitCodes: []
293
335
 
294
336
  # ── dump ─────────────────────────────────────────
295
337
  dump:
@@ -325,6 +367,11 @@ commandSets:
325
367
  sideEffects:
326
368
  - database_read
327
369
  - file_write
370
+ sideEffectNote: >-
371
+ Reads DB schema via pg_dump/mysqldump and writes normalized
372
+ output to schema.sql.
373
+ expectedDurationMs: 10000
374
+ retryableExitCodes: [1]
328
375
 
329
376
  # ── diff ─────────────────────────────────────────
330
377
  diff:
@@ -369,6 +416,11 @@ commandSets:
369
416
  idempotent: true
370
417
  sideEffects:
371
418
  - database_read
419
+ sideEffectNote: >-
420
+ Reads DB schema via pg_dump/mysqldump and compares with
421
+ saved schema.sql. No writes.
422
+ expectedDurationMs: 10000
423
+ retryableExitCodes: [1]
372
424
 
373
425
  # ── status ───────────────────────────────────────
374
426
  status:
@@ -406,6 +458,9 @@ commandSets:
406
458
  idempotent: true
407
459
  sideEffects:
408
460
  - database_read
461
+ sideEffectNote: Reads schema_migrations table and compares with local files.
462
+ expectedDurationMs: 5000
463
+ retryableExitCodes: [1]
409
464
 
410
465
  # ── resolve ──────────────────────────────────────
411
466
  resolve:
@@ -417,6 +472,7 @@ commandSets:
417
472
  addressed by other means.
418
473
  usage:
419
474
  - migraguard resolve 20260301_120000__create_users_table.sql
475
+ - migraguard resolve --dry-run 20260301_120000__create_users_table.sql
420
476
 
421
477
  arguments:
422
478
  - name: file
@@ -426,6 +482,14 @@ commandSets:
426
482
  schema:
427
483
  type: string
428
484
 
485
+ options:
486
+ - name: dry-run
487
+ aliases: [n]
488
+ description: Show what would be resolved without writing to DB.
489
+ schema:
490
+ type: boolean
491
+ default: false
492
+
429
493
  exits:
430
494
  '0':
431
495
  description: Migration marked as skipped.
@@ -447,6 +511,10 @@ commandSets:
447
511
  Re-apply the migration manually if needed.
448
512
  sideEffects:
449
513
  - database_write
514
+ sideEffectNote: Inserts a 'skipped' record into schema_migrations for the given file.
515
+ safeDryRunOption: dry-run
516
+ expectedDurationMs: 3000
517
+ retryableExitCodes: [1]
450
518
  recommendedBeforeUse:
451
519
  - Verify that the failure has been addressed by a subsequent migration or manual fix.
452
520
  - Run migraguard status to confirm the file is in failed state.
@@ -476,6 +544,9 @@ commandSets:
476
544
  requiresConfirmation: false
477
545
  idempotent: true
478
546
  sideEffects: []
547
+ sideEffectNote: Read-only. Lists leaf/latest migration files from metadata.
548
+ expectedDurationMs: 1000
549
+ retryableExitCodes: []
479
550
 
480
551
  # ── verify ───────────────────────────────────────
481
552
  verify:
@@ -512,7 +583,14 @@ commandSets:
512
583
  format: '{options.format}'
513
584
 
514
585
  '1':
586
+ description: Unexpected error (connection error, file parse failure).
587
+ stderr:
588
+ format: text
589
+
590
+ '10':
515
591
  description: One or more migrations failed idempotency verification.
592
+ stdout:
593
+ format: '{options.format}'
516
594
  stderr:
517
595
  format: text
518
596
 
@@ -523,6 +601,11 @@ commandSets:
523
601
  sideEffects:
524
602
  - database_read
525
603
  - database_write
604
+ sideEffectNote: >-
605
+ Creates and destroys a temporary shadow database to verify
606
+ idempotency. Does not modify the primary database.
607
+ expectedDurationMs: 60000
608
+ retryableExitCodes: [1]
526
609
  recommendedBeforeUse:
527
610
  - Ensure the database server is running and accessible.
528
611
 
@@ -562,6 +645,9 @@ commandSets:
562
645
  requiresConfirmation: false
563
646
  idempotent: true
564
647
  sideEffects: []
648
+ sideEffectNote: Read-only. Derives group state from schema_migrations records.
649
+ expectedDurationMs: 3000
650
+ retryableExitCodes: [1]
565
651
 
566
652
  # ── baseline ─────────────────────────────────────
567
653
  baseline:
@@ -573,6 +659,7 @@ commandSets:
573
659
  usage:
574
660
  - migraguard baseline
575
661
  - migraguard baseline --keep-since 20260301_120000__create_users_table.sql
662
+ - migraguard baseline --dry-run
576
663
 
577
664
  options:
578
665
  - name: keep-since
@@ -582,9 +669,16 @@ commandSets:
582
669
  schema:
583
670
  type: string
584
671
 
672
+ - name: dry-run
673
+ aliases: [n]
674
+ description: Show what would be baselined without writing files.
675
+ schema:
676
+ type: boolean
677
+ default: false
678
+
585
679
  exits:
586
680
  '0':
587
- description: Baseline created successfully.
681
+ description: Baseline created successfully (or dry-run preview completed).
588
682
  stdout:
589
683
  format: text
590
684
 
@@ -604,6 +698,12 @@ commandSets:
604
698
  sideEffects:
605
699
  - file_write
606
700
  - file_delete
701
+ sideEffectNote: >-
702
+ Dumps current DB schema to schema.sql, deletes squashed migration
703
+ files, updates metadata.json, and rewrites depends-on references.
704
+ safeDryRunOption: dry-run
705
+ expectedDurationMs: 30000
706
+ retryableExitCodes: []
607
707
  recommendedBeforeUse:
608
708
  - Ensure all migrations have been applied to all environments.
609
709
  - Back up migration files before running.
@@ -650,9 +750,16 @@ commandSets:
650
750
  schema:
651
751
  type: string
652
752
 
753
+ - name: dry-run
754
+ aliases: [n]
755
+ description: Show what would be recorded without writing to DB.
756
+ schema:
757
+ type: boolean
758
+ default: false
759
+
653
760
  exits:
654
761
  '0':
655
- description: Phase state transition recorded.
762
+ description: Phase state transition recorded (or dry-run preview completed).
656
763
  stdout:
657
764
  format: text
658
765
 
@@ -673,6 +780,10 @@ commandSets:
673
780
  status value to manually correct, or reset group state in the database.
674
781
  sideEffects:
675
782
  - database_write
783
+ sideEffectNote: Inserts a phase state transition record into schema_migrations.
784
+ safeDryRunOption: dry-run
785
+ expectedDurationMs: 5000
786
+ retryableExitCodes: [1]
676
787
  recommendedBeforeUse:
677
788
  - Run migraguard group-status <group> to verify current phase state before advancing.
678
789
 
@@ -710,9 +821,16 @@ commandSets:
710
821
  schema:
711
822
  type: string
712
823
 
824
+ - name: dry-run
825
+ aliases: [n]
826
+ description: Show what would be applied without executing SQL.
827
+ schema:
828
+ type: boolean
829
+ default: false
830
+
713
831
  exits:
714
832
  '0':
715
- description: Phase applied successfully.
833
+ description: Phase applied successfully (or dry-run preview completed).
716
834
  stdout:
717
835
  format: text
718
836
 
@@ -733,6 +851,12 @@ commandSets:
733
851
  migration or restore from backup if rollback is needed.
734
852
  sideEffects:
735
853
  - database_write
854
+ sideEffectNote: >-
855
+ Executes a phase SQL file via native CLI and records the
856
+ result in schema_migrations. Uses advisory lock.
857
+ safeDryRunOption: dry-run
858
+ expectedDurationMs: 30000
859
+ retryableExitCodes: []
736
860
  recommendedBeforeUse:
737
861
  - Run migraguard group-status to verify current phase state.
738
862
 
@@ -790,6 +914,11 @@ commandSets:
790
914
  format: text
791
915
 
792
916
  '1':
917
+ description: Unexpected error (connection error, invalid condition syntax).
918
+ stderr:
919
+ format: text
920
+
921
+ '10':
793
922
  description: One or more gate conditions not satisfied.
794
923
  stderr:
795
924
  format: text
@@ -799,6 +928,9 @@ commandSets:
799
928
  requiresConfirmation: false
800
929
  idempotent: true
801
930
  sideEffects: []
931
+ sideEffectNote: Read-only. Evaluates gate conditions against schema_migrations state.
932
+ expectedDurationMs: 5000
933
+ retryableExitCodes: [1]
802
934
 
803
935
  # ── deps ─────────────────────────────────────────
804
936
  deps:
@@ -845,6 +977,11 @@ commandSets:
845
977
  idempotent: true
846
978
  sideEffects:
847
979
  - file_write
980
+ sideEffectNote: >-
981
+ Reads migration files and outputs dependency graph.
982
+ Writes HTML file only when --html is specified.
983
+ expectedDurationMs: 3000
984
+ retryableExitCodes: []
848
985
 
849
986
  # ── audit ─────────────────────────────────────────
850
987
  audit:
@@ -917,7 +1054,7 @@ commandSets:
917
1054
  schema:
918
1055
  type: string
919
1056
  enum: [json, text, yaml]
920
- default: text
1057
+ default: json
921
1058
 
922
1059
  exits:
923
1060
  '0':
@@ -964,7 +1101,7 @@ commandSets:
964
1101
  Filesystem write only when --output is specified.
965
1102
  safeDryRunOption: dry-run
966
1103
  expectedDurationMs: 120000
967
- retryableExitCodes: [1, 12]
1104
+ retryableExitCodes: [12]
968
1105
 
969
1106
  # ── propose-expand-contract ───────────────────────
970
1107
  propose-expand-contract:
@@ -1092,7 +1229,7 @@ commandSets:
1092
1229
  Filesystem write when --output or --output-dir is specified.
1093
1230
  safeDryRunOption: dry-run
1094
1231
  expectedDurationMs: 120000
1095
- retryableExitCodes: [1, 12]
1232
+ retryableExitCodes: [12]
1096
1233
 
1097
1234
  # ── explain ───────────────────────────────────────
1098
1235
  explain:
@@ -1208,12 +1345,14 @@ commandSets:
1208
1345
  Filesystem write only when --output is specified.
1209
1346
  safeDryRunOption: dry-run
1210
1347
  expectedDurationMs: 120000
1211
- retryableExitCodes: [1, 12]
1348
+ retryableExitCodes: [12]
1212
1349
 
1213
1350
  components:
1214
1351
  schemas:
1215
1352
  # ── AI Agent Interoperability Schemas (SSoT) ─────────
1216
1353
 
1354
+ # Conforms to agent-contracts dsl_base/components.yaml#/schemas/agent-evidence.
1355
+ # Canonical property names are kind/target/location/excerpt (NOT type/content/source).
1217
1356
  AgentEvidence:
1218
1357
  type: object
1219
1358
  description: Evidence supporting an agent finding.
@@ -1390,7 +1529,7 @@ components:
1390
1529
  description: >-
1391
1530
  Result from explain command. Contains a human-readable explanation
1392
1531
  of another command's output, suitable for PR comments or release decisions.
1393
- required: [summary, riskLevel, explanation]
1532
+ required: [summary, riskLevel, findings, explanation]
1394
1533
  properties:
1395
1534
  summary:
1396
1535
  type: string
package/dist/cli.js CHANGED
@@ -2224,6 +2224,25 @@ async function commandApply(config, options) {
2224
2224
  orderedFileNames = files.map((f) => f.fileName);
2225
2225
  }
2226
2226
  const fileMap = new Map(files.map((f) => [f.fileName, f]));
2227
+ if (options?.dryRun) {
2228
+ const pending = [];
2229
+ for (const fileName of orderedFileNames) {
2230
+ const fileRecords = recordsByFile.get(fileName) ?? [];
2231
+ const latestRecord = getLatestRecord(fileRecords);
2232
+ if (!latestRecord || latestRecord.status === "failed") {
2233
+ pending.push(fileName);
2234
+ }
2235
+ }
2236
+ if (pending.length === 0) {
2237
+ console.log(chalk.green("All migrations are up to date."));
2238
+ } else {
2239
+ console.log(chalk.cyan(`[dry-run] ${pending.length} migration(s) would be applied:`));
2240
+ for (const f of pending) {
2241
+ console.log(chalk.cyan(` ${f}`));
2242
+ }
2243
+ }
2244
+ return result;
2245
+ }
2227
2246
  const latestFileName = files[files.length - 1].fileName;
2228
2247
  const blockedSet = /* @__PURE__ */ new Set();
2229
2248
  const groupStates = deriveAllGroupStates(allRecords);
@@ -2502,6 +2521,11 @@ async function commandAdvance(config, options) {
2502
2521
  }
2503
2522
  const dbStatus = STATUS_MAP[status];
2504
2523
  const fileName = findPhaseFileName(group, phase, allRecords);
2524
+ if (options.dryRun) {
2525
+ console.log(chalk.cyan(`[dry-run] Would advance "${group}" ${phase} \u2192 ${status}`));
2526
+ console.log(chalk.cyan(` File: ${fileName}, current state: ${previousState}`));
2527
+ return { success: true, previousState, newState: `${phase}:${status}` };
2528
+ }
2505
2529
  await db.insertRecord(fileName, "", dbStatus, {
2506
2530
  migrationClass: "expand_contract",
2507
2531
  phase,
@@ -2627,6 +2651,12 @@ async function commandApplyPhase(config, options) {
2627
2651
  return { success: false, group, phase, error: msg };
2628
2652
  }
2629
2653
  const checksum = await checksumFile(file.filePath);
2654
+ if (options.dryRun) {
2655
+ console.log(chalk.cyan(`[dry-run] Would apply phase "${phase}" for "${group}"`));
2656
+ console.log(chalk.cyan(` File: ${expectedFileName}`));
2657
+ console.log(chalk.cyan(` Checksum: ${checksum}`));
2658
+ return { success: true, group, phase };
2659
+ }
2630
2660
  const psqlResult = await executeSqlFile(config, file.filePath);
2631
2661
  if (psqlResult.success) {
2632
2662
  await db.insertRecord(file.fileName, checksum, "applied", {
@@ -2710,6 +2740,14 @@ async function commandBaseline(config, options) {
2710
2740
  console.log(chalk.yellow("No files to baseline."));
2711
2741
  return { success: true, squashedFiles: [], remainingLeaves: [...keepSet], schemaFile: "" };
2712
2742
  }
2743
+ if (options?.dryRun) {
2744
+ console.log(chalk.cyan(`[dry-run] Would baseline ${squashTargets.length} file(s) into ${config.schemaFile}:`));
2745
+ for (const f of squashTargets) {
2746
+ console.log(chalk.cyan(` squash: ${f}`));
2747
+ }
2748
+ console.log(chalk.cyan(` Remaining: ${[...keepSet].join(", ") || "(none)"}`));
2749
+ return { success: true, squashedFiles: squashTargets, remainingLeaves: [...keepSet], schemaFile: config.schemaFile };
2750
+ }
2713
2751
  const schemaContent = await dumpSchema(config);
2714
2752
  const schemaPath = resolveFromConfig(config, config.schemaFile);
2715
2753
  const baselineIncludes = [];
@@ -2933,39 +2971,50 @@ async function commandNew(config, name, options) {
2933
2971
  const now = /* @__PURE__ */ new Date();
2934
2972
  const primaryDir = config.migrationsDirs[0];
2935
2973
  const migrationsDir = resolveFromConfig(config, primaryDir);
2936
- if (!existsSync(migrationsDir)) {
2974
+ if (!options?.dryRun && !existsSync(migrationsDir)) {
2937
2975
  await mkdir(migrationsDir, { recursive: true });
2938
2976
  }
2939
2977
  if (options?.expandContract) {
2940
- await createExpandContractGroup(config, name, now, existingParsed, primaryDir, migrationsDir);
2978
+ await createExpandContractGroup(config, name, now, existingParsed, primaryDir, migrationsDir, options?.dryRun);
2941
2979
  } else {
2942
- await createSingleFile(config, name, now, existingParsed, primaryDir, migrationsDir);
2980
+ await createSingleFile(config, name, now, existingParsed, primaryDir, migrationsDir, options?.dryRun);
2943
2981
  }
2944
2982
  }
2945
- async function createSingleFile(config, name, now, existingParsed, primaryDir, migrationsDir) {
2983
+ async function createSingleFile(config, name, now, existingParsed, primaryDir, migrationsDir, dryRun) {
2946
2984
  const fileName = generateFileName(name, config.naming, { now, existingParsed });
2947
2985
  const filePath = `${migrationsDir}/${fileName}`;
2948
2986
  if (existsSync(filePath)) {
2949
2987
  throw new Error(`File already exists: ${filePath}`);
2950
2988
  }
2989
+ if (dryRun) {
2990
+ console.log(chalk.cyan(`[dry-run] Would create: ${primaryDir}/${fileName}`));
2991
+ return;
2992
+ }
2951
2993
  const content = TEMPLATE.replace("{description}", name).replace("{timestamp}", now.toISOString());
2952
2994
  await writeFile(filePath, content, "utf-8");
2953
2995
  console.log(chalk.green(`Created: ${primaryDir}/${fileName}`));
2954
2996
  }
2955
- async function createExpandContractGroup(config, name, now, existingParsed, primaryDir, migrationsDir) {
2997
+ async function createExpandContractGroup(config, name, now, existingParsed, primaryDir, migrationsDir, dryRun) {
2956
2998
  const groupDir = generateGroupDirName(name, config.naming, { now, existingParsed });
2957
2999
  const groupPath = `${migrationsDir}/${groupDir}`;
2958
3000
  if (existsSync(groupPath)) {
2959
3001
  throw new Error(`Directory already exists: ${groupPath}`);
2960
3002
  }
2961
- await mkdir(groupPath, { recursive: true });
2962
- const ts = now.toISOString();
2963
3003
  const phaseFiles = [
2964
3004
  { name: "1_expand.sql", template: EXPAND_TEMPLATE },
2965
3005
  { name: "2_backfill.sql", template: BACKFILL_TEMPLATE },
2966
3006
  { name: "3_switch.sql", template: SWITCH_TEMPLATE },
2967
3007
  { name: "4_contract.sql", template: CONTRACT_TEMPLATE }
2968
3008
  ];
3009
+ if (dryRun) {
3010
+ console.log(chalk.cyan(`[dry-run] Would create migration group: ${primaryDir}/${groupDir}/`));
3011
+ for (const pf of phaseFiles) {
3012
+ console.log(chalk.cyan(` ${pf.name}`));
3013
+ }
3014
+ return;
3015
+ }
3016
+ await mkdir(groupPath, { recursive: true });
3017
+ const ts = now.toISOString();
2969
3018
  for (const pf of phaseFiles) {
2970
3019
  const content = pf.template.replace("{group}", groupDir).replace("{timestamp}", ts);
2971
3020
  await writeFile(`${groupPath}/${pf.name}`, content, "utf-8");
@@ -3106,7 +3155,7 @@ ${content}`);
3106
3155
  }
3107
3156
  return updated;
3108
3157
  }
3109
- async function commandSquash(config) {
3158
+ async function commandSquash(config, options) {
3110
3159
  const metadata = await loadMetadata(config);
3111
3160
  const files = await scanMigrations(config);
3112
3161
  const metadataFileSet = new Set(metadata.migrations.map((m) => m.file));
@@ -3115,6 +3164,13 @@ async function commandSquash(config) {
3115
3164
  console.log(chalk.yellow("No new migration files to squash."));
3116
3165
  return;
3117
3166
  }
3167
+ if (options?.dryRun) {
3168
+ console.log(chalk.cyan(`[dry-run] Would squash ${newFiles.length} file(s):`));
3169
+ for (const f of newFiles) {
3170
+ console.log(chalk.cyan(` ${f.fileName}`));
3171
+ }
3172
+ return;
3173
+ }
3118
3174
  if (isDagMode(metadata, config)) {
3119
3175
  await dagSquash(config, newFiles, metadata);
3120
3176
  } else {
@@ -5177,7 +5233,7 @@ function formatGroupState(state) {
5177
5233
  return chalk.cyan(state);
5178
5234
  }
5179
5235
  }
5180
- async function commandResolve(config, fileName) {
5236
+ async function commandResolve(config, fileName, options) {
5181
5237
  const db = createDb(config);
5182
5238
  try {
5183
5239
  await db.connect();
@@ -5194,6 +5250,11 @@ async function commandResolve(config, fileName) {
5194
5250
  `Cannot resolve "${fileName}": latest status is "${latestRecord.status}", expected "failed".`
5195
5251
  );
5196
5252
  }
5253
+ if (options?.dryRun) {
5254
+ console.log(chalk.cyan(`[dry-run] Would resolve: "${fileName}" \u2192 mark as skipped`));
5255
+ console.log(chalk.cyan(` Current status: ${latestRecord.status}, checksum: ${latestRecord.checksum}`));
5256
+ return;
5257
+ }
5197
5258
  await db.insertRecord(fileName, latestRecord.checksum, "skipped");
5198
5259
  console.log(chalk.green(`\u2713 Resolved: "${fileName}" marked as skipped.`));
5199
5260
  console.log(chalk.gray(` Ensure a subsequent forward migration covers the intended changes.`));
@@ -6375,16 +6436,17 @@ async function run(fn) {
6375
6436
  }
6376
6437
  var program = new Command();
6377
6438
  program.name(pkg.name).description(pkg.description).version(pkg.version);
6378
- program.command("new <name>").description("Create a new migration SQL file with UTC timestamp").option("--expand-contract", "Create an expand/contract migration group (Class B)").action((name, opts) => run(async () => {
6439
+ program.command("new <name>").description("Create a new migration SQL file with UTC timestamp").option("--expand-contract", "Create an expand/contract migration group (Class B)").option("-n, --dry-run", "Show what would be created without writing files").action((name, opts) => run(async () => {
6379
6440
  const config = await loadConfig();
6380
- await commandNew(config, name, { expandContract: opts.expandContract });
6441
+ await commandNew(config, name, { expandContract: opts.expandContract, dryRun: opts.dryRun });
6381
6442
  }));
6382
- program.command("apply").description("Apply pending migrations via psql").option("--with-drift-check", "Check schema drift before apply and update dump after").option("--from-baseline", "Apply schema.sql first, then remaining migrations").option("--tag <text>", "Tag to record with applied migrations (e.g. commit hash, release tag)").action((opts) => run(async () => {
6443
+ program.command("apply").description("Apply pending migrations via psql").option("--with-drift-check", "Check schema drift before apply and update dump after").option("--from-baseline", "Apply schema.sql first, then remaining migrations").option("--tag <text>", "Tag to record with applied migrations (e.g. commit hash, release tag)").option("-n, --dry-run", "List pending migrations without applying").action((opts) => run(async () => {
6383
6444
  const config = await loadConfig();
6384
6445
  const result = await commandApply(config, {
6385
6446
  withDriftCheck: opts.withDriftCheck,
6386
6447
  fromBaseline: opts.fromBaseline,
6387
- tag: opts.tag
6448
+ tag: opts.tag,
6449
+ dryRun: opts.dryRun
6388
6450
  });
6389
6451
  if (result.errors.length > 0) process.exit(1);
6390
6452
  }));
@@ -6393,9 +6455,9 @@ program.command("check").description("Verify metadata integrity (no DB connectio
6393
6455
  const result = await commandCheck(config);
6394
6456
  if (!result.ok) process.exit(1);
6395
6457
  }));
6396
- program.command("squash").description("Squash multiple new migration files into one").action(() => run(async () => {
6458
+ program.command("squash").description("Squash multiple new migration files into one").option("-n, --dry-run", "Show what would be squashed without writing files").action((opts) => run(async () => {
6397
6459
  const config = await loadConfig();
6398
- await commandSquash(config);
6460
+ await commandSquash(config, { dryRun: opts.dryRun });
6399
6461
  }));
6400
6462
  program.command("lint").description("Run built-in safety rules on migration files").action(() => run(async () => {
6401
6463
  const config = await loadConfig();
@@ -6415,9 +6477,9 @@ program.command("status").description("Show migration status (applied / pending
6415
6477
  const config = await loadConfig();
6416
6478
  await commandStatus(config);
6417
6479
  }));
6418
- program.command("resolve <file>").description("Mark a failed migration as skipped (requires human judgment)").action((file) => run(async () => {
6480
+ program.command("resolve <file>").description("Mark a failed migration as skipped (requires human judgment)").option("-n, --dry-run", "Show what would be resolved without writing to DB").action((file, opts) => run(async () => {
6419
6481
  const config = await loadConfig();
6420
- await commandResolve(config, file);
6482
+ await commandResolve(config, file, { dryRun: opts.dryRun });
6421
6483
  }));
6422
6484
  program.command("editable").description("List migration files that are currently editable (leaf nodes or latest file)").action(() => run(async () => {
6423
6485
  const config = await loadConfig();
@@ -6432,12 +6494,12 @@ program.command("group-status [group]").description("Show migration group state
6432
6494
  const config = await loadConfig();
6433
6495
  await commandGroupStatus(config, group);
6434
6496
  }));
6435
- program.command("baseline").description("Squash applied migrations into schema.sql baseline").option("--keep-since <file...>", "Keep files from this point forward").action((opts) => run(async () => {
6497
+ program.command("baseline").description("Squash applied migrations into schema.sql baseline").option("--keep-since <file...>", "Keep files from this point forward").option("-n, --dry-run", "Show what would be baselined without writing files").action((opts) => run(async () => {
6436
6498
  const config = await loadConfig();
6437
- const result = await commandBaseline(config, { keepSince: opts.keepSince });
6499
+ const result = await commandBaseline(config, { keepSince: opts.keepSince, dryRun: opts.dryRun });
6438
6500
  if (!result.success) process.exit(1);
6439
6501
  }));
6440
- program.command("advance <group> <phase> <status>").description("Record a phase state transition (for external executor)").option("--tag <text>", "Tag to record (e.g. commit hash, release tag)").action((group, phase, status, opts) => run(async () => {
6502
+ program.command("advance <group> <phase> <status>").description("Record a phase state transition (for external executor)").option("--tag <text>", "Tag to record (e.g. commit hash, release tag)").option("-n, --dry-run", "Show what would be recorded without writing to DB").action((group, phase, status, opts) => run(async () => {
6441
6503
  const config = await loadConfig();
6442
6504
  const validPhases = ["expand", "backfill", "switch", "contract"];
6443
6505
  const validStatuses = ["running", "completed", "failed"];
@@ -6447,18 +6509,20 @@ program.command("advance <group> <phase> <status>").description("Record a phase
6447
6509
  group,
6448
6510
  phase,
6449
6511
  status,
6450
- tag: opts.tag
6512
+ tag: opts.tag,
6513
+ dryRun: opts.dryRun
6451
6514
  });
6452
6515
  if (!result.success) process.exit(1);
6453
6516
  }));
6454
- program.command("apply-phase <group> <phase>").description("Apply a specific phase of a migration group via psql").option("--tag <text>", "Tag to record (e.g. commit hash, release tag)").action((group, phase, opts) => run(async () => {
6517
+ program.command("apply-phase <group> <phase>").description("Apply a specific phase of a migration group via psql").option("--tag <text>", "Tag to record (e.g. commit hash, release tag)").option("-n, --dry-run", "Show what would be applied without executing SQL").action((group, phase, opts) => run(async () => {
6455
6518
  const config = await loadConfig();
6456
6519
  const validPhases = ["expand", "backfill", "switch", "contract"];
6457
6520
  if (!validPhases.includes(phase)) throw new Error(`Invalid phase: ${phase}`);
6458
6521
  const result = await commandApplyPhase(config, {
6459
6522
  group,
6460
6523
  phase,
6461
- tag: opts.tag
6524
+ tag: opts.tag,
6525
+ dryRun: opts.dryRun
6462
6526
  });
6463
6527
  if (!result.success) process.exit(1);
6464
6528
  }));