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 +148 -9
- package/dist/cli.js +87 -23
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -1
- package/docs/cli-reference.md +443 -25
- package/package.json +2 -2
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:
|
|
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: [
|
|
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: [
|
|
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: [
|
|
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
|
}));
|