@quonfig/cli 0.0.34 → 0.0.35
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/README.md +35 -35
- package/dist/commands/migrate.js +17 -10
- package/dist/migrate/local-write.d.ts +19 -2
- package/dist/migrate/local-write.js +102 -4
- package/dist/migrate/migration-report.d.ts +3 -0
- package/dist/migrate/migration-report.js +3 -0
- package/dist/migrate/push-to-cloud.d.ts +4 -1
- package/dist/migrate/push-to-cloud.js +9 -44
- package/dist/migrate/sources/launch/translate.d.ts +16 -0
- package/dist/migrate/sources/launch/translate.js +112 -5
- package/dist/util/clone-and-stack-push.d.ts +6 -2
- package/dist/util/clone-and-stack-push.js +9 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ $ npm install -g @quonfig/cli
|
|
|
14
14
|
$ qfg COMMAND
|
|
15
15
|
running command...
|
|
16
16
|
$ qfg (--version)
|
|
17
|
-
@quonfig/cli/0.0.
|
|
17
|
+
@quonfig/cli/0.0.35 linux-x64 node-v24.4.1
|
|
18
18
|
$ qfg --help [COMMAND]
|
|
19
19
|
USAGE
|
|
20
20
|
$ qfg COMMAND
|
|
@@ -91,7 +91,7 @@ EXAMPLES
|
|
|
91
91
|
$ qfg config-schema --json-schema # full JSON Schema document (copy into your editor)
|
|
92
92
|
```
|
|
93
93
|
|
|
94
|
-
_See code: [src/commands/config-schema.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
94
|
+
_See code: [src/commands/config-schema.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/config-schema.ts)_
|
|
95
95
|
|
|
96
96
|
## `qfg create NAME`
|
|
97
97
|
|
|
@@ -176,7 +176,7 @@ EXAMPLES
|
|
|
176
176
|
$ qfg set-rollout my.new.flag --environment production --true-percent 20
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
-
_See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
179
|
+
_See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/create.ts)_
|
|
180
180
|
|
|
181
181
|
## `qfg delete NAME`
|
|
182
182
|
|
|
@@ -222,7 +222,7 @@ EXAMPLES
|
|
|
222
222
|
$ qfg delete my.config --yes
|
|
223
223
|
```
|
|
224
224
|
|
|
225
|
-
_See code: [src/commands/delete.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
225
|
+
_See code: [src/commands/delete.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/delete.ts)_
|
|
226
226
|
|
|
227
227
|
## `qfg flag info [NAME]`
|
|
228
228
|
|
|
@@ -425,7 +425,7 @@ EXAMPLES
|
|
|
425
425
|
$ qfg generate --targets node-ts -o ./dist # combine with targets
|
|
426
426
|
```
|
|
427
427
|
|
|
428
|
-
_See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
428
|
+
_See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/generate.ts)_
|
|
429
429
|
|
|
430
430
|
## `qfg generate-new-hex-key`
|
|
431
431
|
|
|
@@ -448,7 +448,7 @@ EXAMPLES
|
|
|
448
448
|
$ qfg generate-new-hex-key
|
|
449
449
|
```
|
|
450
450
|
|
|
451
|
-
_See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
451
|
+
_See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/generate-new-hex-key.ts)_
|
|
452
452
|
|
|
453
453
|
## `qfg get [NAME]`
|
|
454
454
|
|
|
@@ -480,7 +480,7 @@ EXAMPLES
|
|
|
480
480
|
$ qfg get my.config.name --environment=production
|
|
481
481
|
```
|
|
482
482
|
|
|
483
|
-
_See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
483
|
+
_See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/get.ts)_
|
|
484
484
|
|
|
485
485
|
## `qfg info [NAME]`
|
|
486
486
|
|
|
@@ -528,7 +528,7 @@ EXAMPLES
|
|
|
528
528
|
$ qfg info my.config.name --exclude-evaluations # skip 24h stats
|
|
529
529
|
```
|
|
530
530
|
|
|
531
|
-
_See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
531
|
+
_See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/info.ts)_
|
|
532
532
|
|
|
533
533
|
## `qfg init [DIRECTORY]`
|
|
534
534
|
|
|
@@ -568,7 +568,7 @@ EXAMPLES
|
|
|
568
568
|
$ qfg init --dry-run
|
|
569
569
|
```
|
|
570
570
|
|
|
571
|
-
_See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
571
|
+
_See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/init.ts)_
|
|
572
572
|
|
|
573
573
|
## `qfg interactive`
|
|
574
574
|
|
|
@@ -598,7 +598,7 @@ EXAMPLES
|
|
|
598
598
|
$ qfg
|
|
599
599
|
```
|
|
600
600
|
|
|
601
|
-
_See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
601
|
+
_See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/interactive.ts)_
|
|
602
602
|
|
|
603
603
|
## `qfg list`
|
|
604
604
|
|
|
@@ -638,7 +638,7 @@ EXAMPLES
|
|
|
638
638
|
$ qfg list --feature-flags
|
|
639
639
|
```
|
|
640
640
|
|
|
641
|
-
_See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
641
|
+
_See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/list.ts)_
|
|
642
642
|
|
|
643
643
|
## `qfg log-level NAME`
|
|
644
644
|
|
|
@@ -703,7 +703,7 @@ EXAMPLES
|
|
|
703
703
|
$ qfg log-level log-level.my-app --target=Chatty --value=INFO --environment=production
|
|
704
704
|
```
|
|
705
705
|
|
|
706
|
-
_See code: [src/commands/log-level.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
706
|
+
_See code: [src/commands/log-level.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/log-level.ts)_
|
|
707
707
|
|
|
708
708
|
## `qfg login`
|
|
709
709
|
|
|
@@ -728,7 +728,7 @@ EXAMPLES
|
|
|
728
728
|
$ qfg login --profile myprofile
|
|
729
729
|
```
|
|
730
730
|
|
|
731
|
-
_See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
731
|
+
_See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/login.ts)_
|
|
732
732
|
|
|
733
733
|
## `qfg logout`
|
|
734
734
|
|
|
@@ -751,7 +751,7 @@ EXAMPLES
|
|
|
751
751
|
$ qfg logout
|
|
752
752
|
```
|
|
753
753
|
|
|
754
|
-
_See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
754
|
+
_See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/logout.ts)_
|
|
755
755
|
|
|
756
756
|
## `qfg migrate`
|
|
757
757
|
|
|
@@ -805,7 +805,7 @@ EXAMPLES
|
|
|
805
805
|
$ qfg migrate --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-repo --staging
|
|
806
806
|
```
|
|
807
807
|
|
|
808
|
-
_See code: [src/commands/migrate.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
808
|
+
_See code: [src/commands/migrate.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/migrate.ts)_
|
|
809
809
|
|
|
810
810
|
## `qfg migrate doctor`
|
|
811
811
|
|
|
@@ -843,7 +843,7 @@ EXAMPLES
|
|
|
843
843
|
$ qfg migrate doctor --json
|
|
844
844
|
```
|
|
845
845
|
|
|
846
|
-
_See code: [src/commands/migrate/doctor.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
846
|
+
_See code: [src/commands/migrate/doctor.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/migrate/doctor.ts)_
|
|
847
847
|
|
|
848
848
|
## `qfg migrate my-code`
|
|
849
849
|
|
|
@@ -875,7 +875,7 @@ EXAMPLES
|
|
|
875
875
|
$ qfg migrate my-code --dry-run
|
|
876
876
|
```
|
|
877
877
|
|
|
878
|
-
_See code: [src/commands/migrate/my-code.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
878
|
+
_See code: [src/commands/migrate/my-code.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/migrate/my-code.ts)_
|
|
879
879
|
|
|
880
880
|
## `qfg migrate status`
|
|
881
881
|
|
|
@@ -906,7 +906,7 @@ EXAMPLES
|
|
|
906
906
|
$ qfg migrate status --json
|
|
907
907
|
```
|
|
908
908
|
|
|
909
|
-
_See code: [src/commands/migrate/status.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
909
|
+
_See code: [src/commands/migrate/status.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/migrate/status.ts)_
|
|
910
910
|
|
|
911
911
|
## `qfg override [NAME] [VALUE]`
|
|
912
912
|
|
|
@@ -961,7 +961,7 @@ EXAMPLES
|
|
|
961
961
|
$ qfg override my.flag true --env=staging
|
|
962
962
|
```
|
|
963
963
|
|
|
964
|
-
_See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
964
|
+
_See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/override.ts)_
|
|
965
965
|
|
|
966
966
|
## `qfg pull`
|
|
967
967
|
|
|
@@ -1008,7 +1008,7 @@ EXAMPLES
|
|
|
1008
1008
|
$ qfg pull # uses QUONFIG_DIR env var
|
|
1009
1009
|
```
|
|
1010
1010
|
|
|
1011
|
-
_See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1011
|
+
_See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/pull.ts)_
|
|
1012
1012
|
|
|
1013
1013
|
## `qfg push`
|
|
1014
1014
|
|
|
@@ -1056,7 +1056,7 @@ EXAMPLES
|
|
|
1056
1056
|
$ qfg push --dir ./our-config --yes
|
|
1057
1057
|
```
|
|
1058
1058
|
|
|
1059
|
-
_See code: [src/commands/push.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1059
|
+
_See code: [src/commands/push.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/push.ts)_
|
|
1060
1060
|
|
|
1061
1061
|
## `qfg schema NAME`
|
|
1062
1062
|
|
|
@@ -1094,7 +1094,7 @@ EXAMPLES
|
|
|
1094
1094
|
$ qfg schema my-schema --set-json-schema=@schemas/my-schema.json --protected
|
|
1095
1095
|
```
|
|
1096
1096
|
|
|
1097
|
-
_See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1097
|
+
_See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/schema.ts)_
|
|
1098
1098
|
|
|
1099
1099
|
## `qfg sdk-key`
|
|
1100
1100
|
|
|
@@ -1121,7 +1121,7 @@ EXAMPLES
|
|
|
1121
1121
|
$ qfg sdk-key revoke <key-id>
|
|
1122
1122
|
```
|
|
1123
1123
|
|
|
1124
|
-
_See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1124
|
+
_See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/sdk-key.ts)_
|
|
1125
1125
|
|
|
1126
1126
|
## `qfg sdk-key create`
|
|
1127
1127
|
|
|
@@ -1153,7 +1153,7 @@ EXAMPLES
|
|
|
1153
1153
|
$ qfg sdk-key create --environment staging --type browser
|
|
1154
1154
|
```
|
|
1155
1155
|
|
|
1156
|
-
_See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1156
|
+
_See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/sdk-key/create.ts)_
|
|
1157
1157
|
|
|
1158
1158
|
## `qfg sdk-key list`
|
|
1159
1159
|
|
|
@@ -1182,7 +1182,7 @@ EXAMPLES
|
|
|
1182
1182
|
$ qfg sdk-key list --environment production
|
|
1183
1183
|
```
|
|
1184
1184
|
|
|
1185
|
-
_See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1185
|
+
_See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/sdk-key/list.ts)_
|
|
1186
1186
|
|
|
1187
1187
|
## `qfg sdk-key revoke KEYID`
|
|
1188
1188
|
|
|
@@ -1209,7 +1209,7 @@ EXAMPLES
|
|
|
1209
1209
|
$ qfg sdk-key revoke a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
|
1210
1210
|
```
|
|
1211
1211
|
|
|
1212
|
-
_See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1212
|
+
_See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/sdk-key/revoke.ts)_
|
|
1213
1213
|
|
|
1214
1214
|
## `qfg set-default [NAME]`
|
|
1215
1215
|
|
|
@@ -1280,7 +1280,7 @@ EXAMPLES
|
|
|
1280
1280
|
$ qfg pull && qfg config-schema
|
|
1281
1281
|
```
|
|
1282
1282
|
|
|
1283
|
-
_See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1283
|
+
_See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/set-default.ts)_
|
|
1284
1284
|
|
|
1285
1285
|
## `qfg set-rollout [NAME]`
|
|
1286
1286
|
|
|
@@ -1357,7 +1357,7 @@ EXAMPLES
|
|
|
1357
1357
|
$ qfg set-rollout my.variant.flag --environment production --weights "a:33,b:33,c:34" --hash-by user.id
|
|
1358
1358
|
```
|
|
1359
1359
|
|
|
1360
|
-
_See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1360
|
+
_See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/set-rollout.ts)_
|
|
1361
1361
|
|
|
1362
1362
|
## `qfg sync`
|
|
1363
1363
|
|
|
@@ -1389,7 +1389,7 @@ EXAMPLES
|
|
|
1389
1389
|
$ qfg sync --watch --dir ./our-config --interval 30
|
|
1390
1390
|
```
|
|
1391
1391
|
|
|
1392
|
-
_See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1392
|
+
_See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/sync.ts)_
|
|
1393
1393
|
|
|
1394
1394
|
## `qfg toggle [NAME]`
|
|
1395
1395
|
|
|
@@ -1491,7 +1491,7 @@ EXAMPLES
|
|
|
1491
1491
|
$ qfg verify --json
|
|
1492
1492
|
```
|
|
1493
1493
|
|
|
1494
|
-
_See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1494
|
+
_See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/verify.ts)_
|
|
1495
1495
|
|
|
1496
1496
|
## `qfg whoami`
|
|
1497
1497
|
|
|
@@ -1514,7 +1514,7 @@ EXAMPLES
|
|
|
1514
1514
|
$ qfg whoami
|
|
1515
1515
|
```
|
|
1516
1516
|
|
|
1517
|
-
_See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1517
|
+
_See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/whoami.ts)_
|
|
1518
1518
|
|
|
1519
1519
|
## `qfg workspace`
|
|
1520
1520
|
|
|
@@ -1537,7 +1537,7 @@ EXAMPLES
|
|
|
1537
1537
|
$ qfg workspace
|
|
1538
1538
|
```
|
|
1539
1539
|
|
|
1540
|
-
_See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1540
|
+
_See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/workspace.ts)_
|
|
1541
1541
|
|
|
1542
1542
|
## `qfg workspace bootstrap`
|
|
1543
1543
|
|
|
@@ -1570,7 +1570,7 @@ EXAMPLES
|
|
|
1570
1570
|
$ qfg workspace bootstrap --dir ./our-config --skip-validate
|
|
1571
1571
|
```
|
|
1572
1572
|
|
|
1573
|
-
_See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1573
|
+
_See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/workspace/bootstrap.ts)_
|
|
1574
1574
|
|
|
1575
1575
|
## `qfg workspace create SLUG`
|
|
1576
1576
|
|
|
@@ -1604,7 +1604,7 @@ EXAMPLES
|
|
|
1604
1604
|
$ qfg workspace create my-team --org 11111111-1111-1111-1111-111111111111
|
|
1605
1605
|
```
|
|
1606
1606
|
|
|
1607
|
-
_See code: [src/commands/workspace/create.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1607
|
+
_See code: [src/commands/workspace/create.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/workspace/create.ts)_
|
|
1608
1608
|
|
|
1609
1609
|
## `qfg workspace switch [SLUG]`
|
|
1610
1610
|
|
|
@@ -1632,7 +1632,7 @@ EXAMPLES
|
|
|
1632
1632
|
$ qfg workspace switch acme/production
|
|
1633
1633
|
```
|
|
1634
1634
|
|
|
1635
|
-
_See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1635
|
+
_See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.0.35/src/commands/workspace/switch.ts)_
|
|
1636
1636
|
<!-- commandsstop -->
|
|
1637
1637
|
|
|
1638
1638
|
## Local Development
|
package/dist/commands/migrate.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { BaseCommand } from '../index.js';
|
|
5
5
|
import { CrossSourceError, assertSourceMatches, readImportState } from '../migrate/import-state.js';
|
|
6
|
-
import { applyLocalMigration } from '../migrate/local-write.js';
|
|
6
|
+
import { applyLocalMigration, MigratorKeyCollisionError } from '../migrate/local-write.js';
|
|
7
7
|
import { buildPushConflictSuggestion } from '../migrate/migrate-suggestion.js';
|
|
8
8
|
import { PushConflictError, PushHookRejectedError } from '../util/clone-and-stack-push.js';
|
|
9
9
|
import { MigratorVerifyError, pushMigrationToCloud } from '../migrate/push-to-cloud.js';
|
|
@@ -150,12 +150,11 @@ export default class Migrate extends BaseCommand {
|
|
|
150
150
|
try {
|
|
151
151
|
const result = await pushMigrationToCloud({
|
|
152
152
|
changes: toProcess,
|
|
153
|
-
commitMessage: buildCommitMessage(flags.from, toProcess.length),
|
|
154
153
|
environments,
|
|
155
154
|
importState,
|
|
156
155
|
localDir: dir,
|
|
157
156
|
remoteUrl: repoUrl,
|
|
158
|
-
reportData: buildReportData(flags.from
|
|
157
|
+
reportData: buildReportData(flags.from),
|
|
159
158
|
source,
|
|
160
159
|
});
|
|
161
160
|
duplicateResolutionsForWarn = result.duplicateResolutions;
|
|
@@ -183,6 +182,9 @@ export default class Migrate extends BaseCommand {
|
|
|
183
182
|
`Fix the issues above (typically in the source system or by editing the local files), then re-run with --push. ` +
|
|
184
183
|
`You can re-run \`qfg verify ${dir}\` locally to confirm the fixes before pushing.`);
|
|
185
184
|
}
|
|
185
|
+
if (error instanceof MigratorKeyCollisionError) {
|
|
186
|
+
return this.err(error.message);
|
|
187
|
+
}
|
|
186
188
|
throw error;
|
|
187
189
|
}
|
|
188
190
|
}
|
|
@@ -194,11 +196,10 @@ export default class Migrate extends BaseCommand {
|
|
|
194
196
|
this.log(`Fetched ${changes.length} change(s) from ${flags.from}; processing ${toProcess.length} into ${dir}.`);
|
|
195
197
|
const localResult = await applyLocalMigration({
|
|
196
198
|
changes: toProcess,
|
|
197
|
-
commitMessage: buildCommitMessage(flags.from, toProcess.length),
|
|
198
199
|
environments,
|
|
199
200
|
importState: localImportState,
|
|
200
201
|
localDir: dir,
|
|
201
|
-
reportData: buildReportData(flags.from
|
|
202
|
+
reportData: buildReportData(flags.from),
|
|
202
203
|
source,
|
|
203
204
|
});
|
|
204
205
|
duplicateResolutionsForWarn = localResult.duplicateResolutions;
|
|
@@ -215,6 +216,8 @@ export default class Migrate extends BaseCommand {
|
|
|
215
216
|
catch (error) {
|
|
216
217
|
if (error instanceof NotYetImplementedError)
|
|
217
218
|
return this.err(error.message);
|
|
219
|
+
if (error instanceof MigratorKeyCollisionError)
|
|
220
|
+
return this.err(error.message);
|
|
218
221
|
throw error;
|
|
219
222
|
}
|
|
220
223
|
finally {
|
|
@@ -247,16 +250,20 @@ function latestChangedAtOf(changes, fallback) {
|
|
|
247
250
|
}
|
|
248
251
|
return latest ?? fallback;
|
|
249
252
|
}
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
253
|
+
function buildReportData(source) {
|
|
254
|
+
// Counts are placeholders; applyLocalMigration / pushMigrationToCloud
|
|
255
|
+
// override `counts` after writing files so they reflect what actually
|
|
256
|
+
// landed on disk (qfg-7eig). The commit message is also derived from those
|
|
257
|
+
// counts inside the apply function — it is not built here.
|
|
254
258
|
return {
|
|
255
259
|
cleanMappings: [],
|
|
256
260
|
counts: {
|
|
261
|
+
configsMigrated: 0,
|
|
257
262
|
environmentsMapped: 0,
|
|
258
|
-
flagsMigrated:
|
|
263
|
+
flagsMigrated: 0,
|
|
259
264
|
itemsSkipped: 0,
|
|
265
|
+
logLevelsMigrated: 0,
|
|
266
|
+
schemasMigrated: 0,
|
|
260
267
|
segmentsMigrated: 0,
|
|
261
268
|
},
|
|
262
269
|
dryRun: false,
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { type ImportState } from './import-state.js';
|
|
2
2
|
import { type MigrationReportData } from './migration-report.js';
|
|
3
3
|
import { type PushIdentity } from '../util/clone-and-stack-push.js';
|
|
4
|
-
import type { CoercedSentinelSummary, DroppedOverrideSummary, DuplicateResolutionSummary, LegacyChange, MigrationSource, SkippedConfigSummary } from './source.js';
|
|
4
|
+
import type { CoercedSentinelSummary, DroppedOverrideSummary, DuplicateResolution, DuplicateResolutionSummary, LegacyChange, MigrationSource, SkippedConfigSummary } from './source.js';
|
|
5
5
|
export interface ApplyLocalMigrationOptions {
|
|
6
6
|
/** Commit author. Defaults to the migrator identity. */
|
|
7
7
|
author?: PushIdentity;
|
|
8
8
|
/** Initial branch name when initializing a fresh repo. Defaults to `main`. */
|
|
9
9
|
branch?: string;
|
|
10
10
|
changes: LegacyChange[];
|
|
11
|
-
commitMessage: string;
|
|
12
11
|
/** Environments discovered from the source (slugified). Written to `quonfig.json` if missing. */
|
|
13
12
|
environments: string[];
|
|
14
13
|
importState: ImportState;
|
|
15
14
|
localDir: string;
|
|
15
|
+
/**
|
|
16
|
+
* Base report data. The migrator overrides `counts` after writing files,
|
|
17
|
+
* since counts must reflect what was actually written to disk (qfg-7eig).
|
|
18
|
+
*/
|
|
16
19
|
reportData: MigrationReportData;
|
|
17
20
|
source: MigrationSource;
|
|
18
21
|
}
|
|
@@ -31,4 +34,18 @@ export interface ApplyLocalMigrationResult {
|
|
|
31
34
|
/** Configs soft-skipped during translate due to invalid source data. Null if none. */
|
|
32
35
|
skippedConfigs: SkippedConfigSummary | null;
|
|
33
36
|
}
|
|
37
|
+
export declare class MigratorKeyCollisionError extends Error {
|
|
38
|
+
readonly destPath: string;
|
|
39
|
+
readonly firstSourceKey: string;
|
|
40
|
+
readonly secondSourceKey: string;
|
|
41
|
+
constructor(destPath: string, firstSourceKey: string, secondSourceKey: string);
|
|
42
|
+
}
|
|
43
|
+
export declare const writeQuonfigFiles: (dir: string, changes: LegacyChange[], source: MigrationSource) => WriteQuonfigFilesResult;
|
|
44
|
+
interface WriteQuonfigFilesResult {
|
|
45
|
+
livePaths: string[];
|
|
46
|
+
resolutions: DuplicateResolution[];
|
|
47
|
+
}
|
|
48
|
+
export declare const buildMigrationCounts: (livePaths: string[], environmentsMapped: number, itemsSkipped: number) => import("./migration-report.js").MigrationReportCounts;
|
|
49
|
+
export declare const buildMigrationCommitMessage: (source: string, counts: import("./migration-report.js").MigrationReportCounts) => string;
|
|
34
50
|
export declare const applyLocalMigration: (opts: ApplyLocalMigrationOptions) => Promise<ApplyLocalMigrationResult>;
|
|
51
|
+
export {};
|
|
@@ -32,11 +32,54 @@ const ensureLocalRepo = async (localDir, branch) => {
|
|
|
32
32
|
await runGit(['init', `--initial-branch=${branch}`, localDir]);
|
|
33
33
|
return 'initialized';
|
|
34
34
|
};
|
|
35
|
-
|
|
35
|
+
export class MigratorKeyCollisionError extends Error {
|
|
36
|
+
destPath;
|
|
37
|
+
firstSourceKey;
|
|
38
|
+
secondSourceKey;
|
|
39
|
+
constructor(destPath, firstSourceKey, secondSourceKey) {
|
|
40
|
+
super(`Two source keys collide on destination ${destPath} after key normalization: ` +
|
|
41
|
+
`"${firstSourceKey}" and "${secondSourceKey}". Quonfig keys must be globally unique within a type tree. ` +
|
|
42
|
+
`Rename one in the source system and re-run.`);
|
|
43
|
+
this.destPath = destPath;
|
|
44
|
+
this.firstSourceKey = firstSourceKey;
|
|
45
|
+
this.secondSourceKey = secondSourceKey;
|
|
46
|
+
this.name = 'MigratorKeyCollisionError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const ensureNoNestedPath = (relativePath) => {
|
|
50
|
+
// qfg-qhk1: every output must be exactly `<type-dir>/<flat-key>.json` —
|
|
51
|
+
// never nested. If we ever emit a nested path, refuse to write so we surface
|
|
52
|
+
// it loudly rather than silently producing dirs that lose the flat-file
|
|
53
|
+
// contract (and break tombstone cleanup on subsequent runs).
|
|
54
|
+
const parts = relativePath.split('/');
|
|
55
|
+
if (parts.length !== 2) {
|
|
56
|
+
throw new Error(`Migrator refusing to write nested path "${relativePath}" — outputs must be flat <type-dir>/<key>.json. ` +
|
|
57
|
+
`This is a bug in the source's translate() — it should normalize key separators before computing the path.`);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const rmdirIfEmpty = (dir) => {
|
|
61
|
+
try {
|
|
62
|
+
fs.rmdirSync(dir);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const e = error;
|
|
66
|
+
// ENOTEMPTY: still has contents (expected, leave alone). ENOENT: already gone.
|
|
67
|
+
if (e.code !== 'ENOTEMPTY' && e.code !== 'ENOENT')
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
export const writeQuonfigFiles = (dir, changes, source) => {
|
|
36
72
|
const livePaths = new Map();
|
|
73
|
+
// qfg-qhk1: track which source key wrote each destination path. If a second,
|
|
74
|
+
// different source key tries to write to the same destination, that is a
|
|
75
|
+
// post-normalization collision (e.g. source had keys `foo/bar` and `foo.bar`,
|
|
76
|
+
// both normalize to `foo.bar`). Throw with a clear message.
|
|
77
|
+
const pathOwners = new Map();
|
|
37
78
|
for (const change of changes) {
|
|
38
79
|
const files = source.translate(change);
|
|
80
|
+
const sourceKey = change.key ?? '(unknown)';
|
|
39
81
|
for (const file of files) {
|
|
82
|
+
ensureNoNestedPath(file.path);
|
|
40
83
|
const full = path.join(dir, file.path);
|
|
41
84
|
if (file.deleted) {
|
|
42
85
|
try {
|
|
@@ -47,14 +90,25 @@ const writeQuonfigFiles = (dir, changes, source) => {
|
|
|
47
90
|
throw error;
|
|
48
91
|
}
|
|
49
92
|
livePaths.delete(file.path);
|
|
93
|
+
pathOwners.delete(file.path);
|
|
94
|
+
// qfg-qhk1: clean up any empty parent dirs left over from prior runs
|
|
95
|
+
// that wrote under a nested layout. (New runs never create nested
|
|
96
|
+
// dirs because of normalizeKey + ensureNoNestedPath, but stale dirs
|
|
97
|
+
// from older versions on disk would otherwise persist.)
|
|
98
|
+
rmdirIfEmpty(path.dirname(full));
|
|
50
99
|
}
|
|
51
100
|
else {
|
|
52
101
|
if (file.contents === undefined) {
|
|
53
102
|
throw new Error(`translate() emitted a write op for ${file.path} without contents`);
|
|
54
103
|
}
|
|
104
|
+
const existingOwner = pathOwners.get(file.path);
|
|
105
|
+
if (existingOwner !== undefined && existingOwner !== sourceKey) {
|
|
106
|
+
throw new MigratorKeyCollisionError(file.path, existingOwner, sourceKey);
|
|
107
|
+
}
|
|
55
108
|
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
56
109
|
fs.writeFileSync(full, file.contents);
|
|
57
110
|
livePaths.set(file.path, true);
|
|
111
|
+
pathOwners.set(file.path, sourceKey);
|
|
58
112
|
}
|
|
59
113
|
}
|
|
60
114
|
}
|
|
@@ -71,7 +125,47 @@ const writeQuonfigFiles = (dir, changes, source) => {
|
|
|
71
125
|
livePaths.delete(toDelete);
|
|
72
126
|
}
|
|
73
127
|
}
|
|
74
|
-
return resolutions;
|
|
128
|
+
return { livePaths: [...livePaths.keys()], resolutions };
|
|
129
|
+
};
|
|
130
|
+
const countsFromLivePaths = (livePaths) => {
|
|
131
|
+
let flagsMigrated = 0;
|
|
132
|
+
let configsMigrated = 0;
|
|
133
|
+
let segmentsMigrated = 0;
|
|
134
|
+
let schemasMigrated = 0;
|
|
135
|
+
let logLevelsMigrated = 0;
|
|
136
|
+
for (const p of livePaths) {
|
|
137
|
+
if (p.startsWith('feature-flags/'))
|
|
138
|
+
flagsMigrated++;
|
|
139
|
+
else if (p.startsWith('configs/'))
|
|
140
|
+
configsMigrated++;
|
|
141
|
+
else if (p.startsWith('segments/'))
|
|
142
|
+
segmentsMigrated++;
|
|
143
|
+
else if (p.startsWith('schemas/'))
|
|
144
|
+
schemasMigrated++;
|
|
145
|
+
else if (p.startsWith('log-levels/'))
|
|
146
|
+
logLevelsMigrated++;
|
|
147
|
+
}
|
|
148
|
+
return { configsMigrated, flagsMigrated, logLevelsMigrated, schemasMigrated, segmentsMigrated };
|
|
149
|
+
};
|
|
150
|
+
export const buildMigrationCounts = (livePaths, environmentsMapped, itemsSkipped) => ({
|
|
151
|
+
...countsFromLivePaths(livePaths),
|
|
152
|
+
environmentsMapped,
|
|
153
|
+
itemsSkipped,
|
|
154
|
+
});
|
|
155
|
+
export const buildMigrationCommitMessage = (source, counts) => {
|
|
156
|
+
const parts = [];
|
|
157
|
+
if (counts.flagsMigrated > 0)
|
|
158
|
+
parts.push(`${counts.flagsMigrated} flag(s)`);
|
|
159
|
+
if (counts.configsMigrated > 0)
|
|
160
|
+
parts.push(`${counts.configsMigrated} config(s)`);
|
|
161
|
+
if (counts.segmentsMigrated > 0)
|
|
162
|
+
parts.push(`${counts.segmentsMigrated} segment(s)`);
|
|
163
|
+
if (counts.schemasMigrated > 0)
|
|
164
|
+
parts.push(`${counts.schemasMigrated} schema(s)`);
|
|
165
|
+
if (counts.logLevelsMigrated > 0)
|
|
166
|
+
parts.push(`${counts.logLevelsMigrated} log-level(s)`);
|
|
167
|
+
const summary = parts.length === 0 ? 'no objects' : parts.join(', ');
|
|
168
|
+
return `migrator: imported ${summary} from ${source}`;
|
|
75
169
|
};
|
|
76
170
|
export const applyLocalMigration = async (opts) => {
|
|
77
171
|
const branch = opts.branch ?? 'main';
|
|
@@ -80,15 +174,18 @@ export const applyLocalMigration = async (opts) => {
|
|
|
80
174
|
await runGit(['-C', opts.localDir, 'config', 'user.name', author.name]);
|
|
81
175
|
await runGit(['-C', opts.localDir, 'config', 'user.email', author.email]);
|
|
82
176
|
ensureQuonfigJson(opts.localDir, opts.environments);
|
|
83
|
-
const resolutionEntries = writeQuonfigFiles(opts.localDir, opts.changes, opts.source);
|
|
177
|
+
const { livePaths, resolutions: resolutionEntries } = writeQuonfigFiles(opts.localDir, opts.changes, opts.source);
|
|
84
178
|
removeQfFromGitignore(opts.localDir);
|
|
85
179
|
writeImportState(opts.localDir, opts.importState);
|
|
86
180
|
const droppedOverrides = opts.source.getDroppedOverrides?.() ?? null;
|
|
87
181
|
const skippedConfigs = opts.source.getSkippedConfigs?.() ?? null;
|
|
88
182
|
const coercedSentinels = opts.source.getCoercedSentinels?.() ?? null;
|
|
89
183
|
const duplicateResolutions = resolutionEntries.length > 0 ? { entries: resolutionEntries, total: resolutionEntries.length } : null;
|
|
184
|
+
const skippedTotal = (skippedConfigs?.total ?? 0) + resolutionEntries.reduce((sum, r) => sum + r.deleted.length, 0);
|
|
185
|
+
const counts = buildMigrationCounts(livePaths, opts.environments.length, skippedTotal);
|
|
90
186
|
const reportData = {
|
|
91
187
|
...opts.reportData,
|
|
188
|
+
counts,
|
|
92
189
|
...(coercedSentinels ? { coercedSentinels } : {}),
|
|
93
190
|
...(droppedOverrides ? { droppedOverrides } : {}),
|
|
94
191
|
...(duplicateResolutions ? { duplicateResolutions } : {}),
|
|
@@ -107,8 +204,9 @@ export const applyLocalMigration = async (opts) => {
|
|
|
107
204
|
skippedConfigs,
|
|
108
205
|
};
|
|
109
206
|
}
|
|
207
|
+
const commitMessage = buildMigrationCommitMessage(opts.source.name, counts);
|
|
110
208
|
await spawnGit(['-C', opts.localDir, 'commit', '-F', '-'], {
|
|
111
|
-
stdin:
|
|
209
|
+
stdin: commitMessage,
|
|
112
210
|
env: {
|
|
113
211
|
GIT_AUTHOR_EMAIL: author.email,
|
|
114
212
|
GIT_AUTHOR_NAME: author.name,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { IdentifierMap } from './identifier-map.js';
|
|
2
2
|
import type { CoercedSentinelSummary, DroppedOverrideSummary, DuplicateResolutionSummary, SkippedConfigSummary } from './source.js';
|
|
3
3
|
export interface MigrationReportCounts {
|
|
4
|
+
configsMigrated: number;
|
|
4
5
|
environmentsMapped: number;
|
|
5
6
|
flagsMigrated: number;
|
|
6
7
|
itemsSkipped: number;
|
|
8
|
+
logLevelsMigrated: number;
|
|
9
|
+
schemasMigrated: number;
|
|
7
10
|
segmentsMigrated: number;
|
|
8
11
|
}
|
|
9
12
|
export interface CleanMapping {
|
|
@@ -5,7 +5,10 @@ const renderCounts = (counts) => [
|
|
|
5
5
|
'## Counts',
|
|
6
6
|
'',
|
|
7
7
|
`- Flags migrated: ${counts.flagsMigrated}`,
|
|
8
|
+
`- Configs migrated: ${counts.configsMigrated}`,
|
|
8
9
|
`- Segments migrated: ${counts.segmentsMigrated}`,
|
|
10
|
+
`- Schemas migrated: ${counts.schemasMigrated}`,
|
|
11
|
+
`- Log levels migrated: ${counts.logLevelsMigrated}`,
|
|
9
12
|
`- Environments mapped: ${counts.environmentsMapped}`,
|
|
10
13
|
`- Items skipped: ${counts.itemsSkipped}`,
|
|
11
14
|
].join('\n');
|
|
@@ -17,7 +17,6 @@ export declare class MigratorVerifyError extends Error {
|
|
|
17
17
|
export interface PushMigrationToCloudOptions {
|
|
18
18
|
branch?: string;
|
|
19
19
|
changes: LegacyChange[];
|
|
20
|
-
commitMessage: string;
|
|
21
20
|
/**
|
|
22
21
|
* Source-side environments (slugified). Additively merged into the target
|
|
23
22
|
* workspace's quonfig.json so flag files that reference these envs verify
|
|
@@ -27,6 +26,10 @@ export interface PushMigrationToCloudOptions {
|
|
|
27
26
|
importState: ImportState;
|
|
28
27
|
localDir: string;
|
|
29
28
|
remoteUrl: string;
|
|
29
|
+
/**
|
|
30
|
+
* Base report data. The migrator overrides `counts` after writing files,
|
|
31
|
+
* since counts must reflect what was actually written to disk (qfg-7eig).
|
|
32
|
+
*/
|
|
30
33
|
reportData: MigrationReportData;
|
|
31
34
|
source: MigrationSource;
|
|
32
35
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
import { removeQfFromGitignore, writeImportState } from './import-state.js';
|
|
4
|
+
import { buildMigrationCommitMessage, buildMigrationCounts, writeQuonfigFiles } from './local-write.js';
|
|
4
5
|
import { writeMigrationReport } from './migration-report.js';
|
|
5
|
-
import { detectDuplicateKeys } from './sources/launch/translate.js';
|
|
6
6
|
import { cloneAndStackPush, } from '../util/clone-and-stack-push.js';
|
|
7
7
|
import { formatResult, validateWorkspace } from '../verify/validate.js';
|
|
8
8
|
/**
|
|
@@ -23,47 +23,6 @@ export class MigratorVerifyError extends Error {
|
|
|
23
23
|
this.result = result;
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
-
const writeQuonfigFiles = (dir, changes, source) => {
|
|
27
|
-
const livePaths = new Map();
|
|
28
|
-
for (const change of changes) {
|
|
29
|
-
const files = source.translate(change);
|
|
30
|
-
for (const file of files) {
|
|
31
|
-
const full = path.join(dir, file.path);
|
|
32
|
-
if (file.deleted) {
|
|
33
|
-
try {
|
|
34
|
-
fs.unlinkSync(full);
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
if (error.code !== 'ENOENT')
|
|
38
|
-
throw error;
|
|
39
|
-
}
|
|
40
|
-
livePaths.delete(file.path);
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
if (file.contents === undefined) {
|
|
44
|
-
throw new Error(`translate() emitted a write op for ${file.path} without contents`);
|
|
45
|
-
}
|
|
46
|
-
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
47
|
-
fs.writeFileSync(full, file.contents);
|
|
48
|
-
livePaths.set(file.path, true);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
const resolutions = detectDuplicateKeys([...livePaths.keys()].map((p) => ({ path: p })));
|
|
53
|
-
for (const resolution of resolutions) {
|
|
54
|
-
for (const toDelete of resolution.deleted) {
|
|
55
|
-
try {
|
|
56
|
-
fs.unlinkSync(path.join(dir, toDelete));
|
|
57
|
-
}
|
|
58
|
-
catch (error) {
|
|
59
|
-
if (error.code !== 'ENOENT')
|
|
60
|
-
throw error;
|
|
61
|
-
}
|
|
62
|
-
livePaths.delete(toDelete);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return resolutions;
|
|
66
|
-
};
|
|
67
26
|
const QUONFIG_JSON_FILENAME = 'quonfig.json';
|
|
68
27
|
const mergeEnvironmentsIntoQuonfigJson = (dir, sourceEnvs) => {
|
|
69
28
|
const quonfigPath = path.join(dir, QUONFIG_JSON_FILENAME);
|
|
@@ -88,12 +47,13 @@ export const pushMigrationToCloud = async (opts) => {
|
|
|
88
47
|
let droppedOverrides = null;
|
|
89
48
|
let duplicateResolutions = null;
|
|
90
49
|
let skippedConfigs = null;
|
|
50
|
+
let computedCommitMessage = `migrator: imported 0 objects from ${opts.source.name}`;
|
|
91
51
|
const cloneOpts = {
|
|
92
52
|
applyDelta(dir) {
|
|
93
53
|
if (opts.environments && opts.environments.length > 0) {
|
|
94
54
|
mergeEnvironmentsIntoQuonfigJson(dir, opts.environments);
|
|
95
55
|
}
|
|
96
|
-
const resolutionEntries = writeQuonfigFiles(dir, opts.changes, opts.source);
|
|
56
|
+
const { livePaths, resolutions: resolutionEntries } = writeQuonfigFiles(dir, opts.changes, opts.source);
|
|
97
57
|
removeQfFromGitignore(dir);
|
|
98
58
|
writeImportState(dir, opts.importState);
|
|
99
59
|
droppedOverrides = opts.source.getDroppedOverrides?.() ?? null;
|
|
@@ -101,8 +61,13 @@ export const pushMigrationToCloud = async (opts) => {
|
|
|
101
61
|
coercedSentinels = opts.source.getCoercedSentinels?.() ?? null;
|
|
102
62
|
duplicateResolutions =
|
|
103
63
|
resolutionEntries.length > 0 ? { entries: resolutionEntries, total: resolutionEntries.length } : null;
|
|
64
|
+
const skippedTotal = (skippedConfigs?.total ?? 0) + resolutionEntries.reduce((sum, r) => sum + r.deleted.length, 0);
|
|
65
|
+
const environmentsCount = opts.environments?.length ?? 0;
|
|
66
|
+
const counts = buildMigrationCounts(livePaths, environmentsCount, skippedTotal);
|
|
67
|
+
computedCommitMessage = buildMigrationCommitMessage(opts.source.name, counts);
|
|
104
68
|
const reportData = {
|
|
105
69
|
...opts.reportData,
|
|
70
|
+
counts,
|
|
106
71
|
...(coercedSentinels ? { coercedSentinels } : {}),
|
|
107
72
|
...(droppedOverrides ? { droppedOverrides } : {}),
|
|
108
73
|
...(duplicateResolutions ? { duplicateResolutions } : {}),
|
|
@@ -117,7 +82,7 @@ export const pushMigrationToCloud = async (opts) => {
|
|
|
117
82
|
if (!verifyResult.valid)
|
|
118
83
|
throw new MigratorVerifyError(verifyResult);
|
|
119
84
|
},
|
|
120
|
-
commitMessage:
|
|
85
|
+
commitMessage: () => computedCommitMessage,
|
|
121
86
|
localDir: opts.localDir,
|
|
122
87
|
remoteUrl: opts.remoteUrl,
|
|
123
88
|
};
|
|
@@ -11,6 +11,22 @@ export declare class InvalidSourceConfigError extends Error {
|
|
|
11
11
|
export declare function slugify(value: string): string;
|
|
12
12
|
export declare function isLegacyLogLevel(type: string): boolean;
|
|
13
13
|
export declare function normalizeLogLevelKey(key: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* qfg-qhk1: Quonfig keys cannot contain `/` — keys are stored as flat dotted
|
|
16
|
+
* filenames, and a `/` in a source key would create a nested directory tree
|
|
17
|
+
* (data-loss-on-tombstone, cross-key collisions). Normalize `/` to `.` so the
|
|
18
|
+
* destination is always flat.
|
|
19
|
+
*/
|
|
20
|
+
export declare function normalizeKey(key: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* qfg-l18w: Operators Quonfig accepts in rule criteria. Mirrors the
|
|
23
|
+
* `OperatorSchema` enum in cli/src/verify/validate.ts (and app-quonfig's
|
|
24
|
+
* config-schemas.ts). If we emit an operator outside this set, qfg verify
|
|
25
|
+
* fails — which is what was happening for `PROP_SEMVER_*` in the FormHealth
|
|
26
|
+
* dry-run before this check existed. Keep this list in lockstep with the
|
|
27
|
+
* verify schema; a CI test in this file enforces parity.
|
|
28
|
+
*/
|
|
29
|
+
export declare const QUONFIG_SUPPORTED_OPERATORS: Set<string>;
|
|
14
30
|
export declare function transformConfig(config: LaunchConfig, envIdMap: Record<string, string>, onDroppedEnv?: (envId: string) => void, onCoercedSentinel?: (envId: string, valueType: string) => void): Record<string, unknown>;
|
|
15
31
|
export declare function getOutputPath(type: string, key: string): string;
|
|
16
32
|
/**
|
|
@@ -23,6 +23,106 @@ export function isLegacyLogLevel(type) {
|
|
|
23
23
|
export function normalizeLogLevelKey(key) {
|
|
24
24
|
return key.startsWith('log-level.') ? key : `log-level.${key}`;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* qfg-qhk1: Quonfig keys cannot contain `/` — keys are stored as flat dotted
|
|
28
|
+
* filenames, and a `/` in a source key would create a nested directory tree
|
|
29
|
+
* (data-loss-on-tombstone, cross-key collisions). Normalize `/` to `.` so the
|
|
30
|
+
* destination is always flat.
|
|
31
|
+
*/
|
|
32
|
+
export function normalizeKey(key) {
|
|
33
|
+
return key.replaceAll('/', '.');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* qfg-l18w: Operators Quonfig accepts in rule criteria. Mirrors the
|
|
37
|
+
* `OperatorSchema` enum in cli/src/verify/validate.ts (and app-quonfig's
|
|
38
|
+
* config-schemas.ts). If we emit an operator outside this set, qfg verify
|
|
39
|
+
* fails — which is what was happening for `PROP_SEMVER_*` in the FormHealth
|
|
40
|
+
* dry-run before this check existed. Keep this list in lockstep with the
|
|
41
|
+
* verify schema; a CI test in this file enforces parity.
|
|
42
|
+
*/
|
|
43
|
+
export const QUONFIG_SUPPORTED_OPERATORS = new Set([
|
|
44
|
+
'ALWAYS_TRUE',
|
|
45
|
+
'PROP_IS_ONE_OF',
|
|
46
|
+
'PROP_IS_NOT_ONE_OF',
|
|
47
|
+
'PROP_STARTS_WITH_ONE_OF',
|
|
48
|
+
'PROP_DOES_NOT_START_WITH_ONE_OF',
|
|
49
|
+
'PROP_ENDS_WITH_ONE_OF',
|
|
50
|
+
'PROP_DOES_NOT_END_WITH_ONE_OF',
|
|
51
|
+
'PROP_CONTAINS_ONE_OF',
|
|
52
|
+
'PROP_DOES_NOT_CONTAIN_ONE_OF',
|
|
53
|
+
'PROP_LESS_THAN',
|
|
54
|
+
'PROP_LESS_THAN_OR_EQUAL',
|
|
55
|
+
'PROP_GREATER_THAN',
|
|
56
|
+
'PROP_GREATER_THAN_OR_EQUAL',
|
|
57
|
+
'PROP_BEFORE',
|
|
58
|
+
'PROP_AFTER',
|
|
59
|
+
'PROP_MATCHES',
|
|
60
|
+
'PROP_DOES_NOT_MATCH',
|
|
61
|
+
'IN_SEG',
|
|
62
|
+
'NOT_IN_SEG',
|
|
63
|
+
'IN_INT_RANGE',
|
|
64
|
+
'LOOKUP_KEY_IN',
|
|
65
|
+
'LOOKUP_KEY_NOT_IN',
|
|
66
|
+
]);
|
|
67
|
+
/**
|
|
68
|
+
* qfg-l18w: Walk every rule criterion in a transformed config and fail-fast at
|
|
69
|
+
* migrate time if it carries an operator Quonfig does not understand. The
|
|
70
|
+
* primary offender today is `PROP_SEMVER_*` (Launch supports semver
|
|
71
|
+
* comparisons; Quonfig does not have a semver operator at all). Without this
|
|
72
|
+
* check the migrator silently writes invalid JSON and the failure surfaces
|
|
73
|
+
* later at qfg-verify — the customer's local file is broken and re-runs
|
|
74
|
+
* re-clobber any manual fix.
|
|
75
|
+
*
|
|
76
|
+
* Routes through `InvalidSourceConfigError` so the failure is per-config (the
|
|
77
|
+
* rest of the run still produces a usable workspace, and the offending key
|
|
78
|
+
* lands in MIGRATION_REPORT.md's "Skipped invalid configs" section with a
|
|
79
|
+
* specific reason — exactly what the customer needs to take to support).
|
|
80
|
+
*/
|
|
81
|
+
function collectUnsupportedOperators(rules) {
|
|
82
|
+
if (!Array.isArray(rules))
|
|
83
|
+
return [];
|
|
84
|
+
const found = [];
|
|
85
|
+
for (const rule of rules) {
|
|
86
|
+
if (!rule || typeof rule !== 'object')
|
|
87
|
+
continue;
|
|
88
|
+
const r = rule;
|
|
89
|
+
if (!Array.isArray(r.criteria))
|
|
90
|
+
continue;
|
|
91
|
+
for (const crit of r.criteria) {
|
|
92
|
+
if (!crit || typeof crit !== 'object')
|
|
93
|
+
continue;
|
|
94
|
+
const c = crit;
|
|
95
|
+
if (typeof c.operator !== 'string')
|
|
96
|
+
continue;
|
|
97
|
+
if (!QUONFIG_SUPPORTED_OPERATORS.has(c.operator)) {
|
|
98
|
+
found.push(c.operator);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return found;
|
|
103
|
+
}
|
|
104
|
+
function validateOperators(out) {
|
|
105
|
+
const offenders = new Set();
|
|
106
|
+
const defaultSection = out.default;
|
|
107
|
+
if (defaultSection) {
|
|
108
|
+
for (const op of collectUnsupportedOperators(defaultSection.rules))
|
|
109
|
+
offenders.add(op);
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(out.environments)) {
|
|
112
|
+
for (const env of out.environments) {
|
|
113
|
+
for (const op of collectUnsupportedOperators(env.rules))
|
|
114
|
+
offenders.add(op);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (offenders.size === 0)
|
|
118
|
+
return;
|
|
119
|
+
const key = String(out.key ?? 'unknown');
|
|
120
|
+
const sorted = [...offenders].sort();
|
|
121
|
+
throw new InvalidSourceConfigError(`Unsupported operator(s) for "${key}": ${sorted.join(', ')}. ` +
|
|
122
|
+
`Quonfig does not currently translate these from the source schema. ` +
|
|
123
|
+
`Recreate the rule in the source system using a supported operator (see qfg config-schema), ` +
|
|
124
|
+
`or contact support@quonfig.com if you need this operator added to Quonfig.`);
|
|
125
|
+
}
|
|
26
126
|
function zeroValue(valueType) {
|
|
27
127
|
switch (valueType) {
|
|
28
128
|
case 'bool': {
|
|
@@ -148,6 +248,11 @@ function normalizeJsonValuesInConfig(out) {
|
|
|
148
248
|
}
|
|
149
249
|
export function transformConfig(config, envIdMap, onDroppedEnv, onCoercedSentinel) {
|
|
150
250
|
const out = JSON.parse(JSON.stringify(config));
|
|
251
|
+
// qfg-qhk1: normalize `/` to `.` in the key so the JSON `key` field matches
|
|
252
|
+
// the flat dotted filename the migrator will write.
|
|
253
|
+
if (typeof out.key === 'string') {
|
|
254
|
+
out.key = normalizeKey(out.key);
|
|
255
|
+
}
|
|
151
256
|
delete out.changedBy;
|
|
152
257
|
delete out.modifiedAt;
|
|
153
258
|
delete out.createdAt;
|
|
@@ -244,25 +349,27 @@ export function transformConfig(config, envIdMap, onDroppedEnv, onCoercedSentine
|
|
|
244
349
|
}
|
|
245
350
|
}
|
|
246
351
|
normalizeJsonValuesInConfig(out);
|
|
352
|
+
validateOperators(out);
|
|
247
353
|
return out;
|
|
248
354
|
}
|
|
249
355
|
export function getOutputPath(type, key) {
|
|
250
356
|
const normalizedType = type.toUpperCase();
|
|
357
|
+
const k = normalizeKey(key);
|
|
251
358
|
switch (normalizedType) {
|
|
252
359
|
case 'FEATURE_FLAG': {
|
|
253
|
-
return `feature-flags/${
|
|
360
|
+
return `feature-flags/${k}.json`;
|
|
254
361
|
}
|
|
255
362
|
case 'LOG_LEVEL_V2': {
|
|
256
|
-
return `log-levels/${normalizeLogLevelKey(
|
|
363
|
+
return `log-levels/${normalizeLogLevelKey(k)}.json`;
|
|
257
364
|
}
|
|
258
365
|
case 'SCHEMA': {
|
|
259
|
-
return `schemas/${
|
|
366
|
+
return `schemas/${k}.json`;
|
|
260
367
|
}
|
|
261
368
|
case 'SEGMENT': {
|
|
262
|
-
return `segments/${
|
|
369
|
+
return `segments/${k}.json`;
|
|
263
370
|
}
|
|
264
371
|
default: {
|
|
265
|
-
return `configs/${
|
|
372
|
+
return `configs/${k}.json`;
|
|
266
373
|
}
|
|
267
374
|
}
|
|
268
375
|
}
|
|
@@ -13,8 +13,12 @@ export interface CloneAndStackPushOptions {
|
|
|
13
13
|
author?: PushIdentity;
|
|
14
14
|
/** Branch to track and push. Defaults to `main`. */
|
|
15
15
|
branch?: string;
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Commit message for the delta. Pass a function when the message depends on
|
|
18
|
+
* counts that are only known after `applyDelta` runs (qfg-7eig). The function
|
|
19
|
+
* is invoked after applyDelta but before commit.
|
|
20
|
+
*/
|
|
21
|
+
commitMessage: string | (() => string);
|
|
18
22
|
/** Where to clone into, or an existing clone to reuse. */
|
|
19
23
|
localDir: string;
|
|
20
24
|
/** Gitea repo URL (may include token). */
|
|
@@ -62,7 +62,13 @@ const ensureCloneOrReuse = async (remoteUrl, localDir, branch) => {
|
|
|
62
62
|
await runGit(localDir, ['merge', '--ff-only', `origin/${branch}`]);
|
|
63
63
|
return 'reused';
|
|
64
64
|
}
|
|
65
|
-
throw new Error(`Local dir ${localDir} is a git repo but its origin (${origin ? redactToken(origin) : 'unset'}) does not match the target remote
|
|
65
|
+
throw new Error(`Local dir ${localDir} is a git repo but its origin (${origin ? redactToken(origin) : 'unset'}) does not match the target remote.\n` +
|
|
66
|
+
`Refuse to clobber.\n\n` +
|
|
67
|
+
`Most likely cause: you ran \`qfg migrate --dir ${localDir}\` first (which \`git init\`s a fresh repo with no remote) and are now trying to \`--push\` from the same dir.\n\n` +
|
|
68
|
+
`Pick one of:\n` +
|
|
69
|
+
` - Re-run with \`--push\` against a NEW empty/non-existent path (the canonical flow).\n` +
|
|
70
|
+
` - Run \`qfg pull --workspace <org/slug> --dir ${localDir}-clone\` to seed a fresh clone, then \`qfg migrate --dir ${localDir}-clone --workspace <org/slug> --push\`.\n` +
|
|
71
|
+
`Docs: https://docs.quonfig.com/docs/migrating/from-launch (Flow B).`);
|
|
66
72
|
}
|
|
67
73
|
if (fs.existsSync(localDir)) {
|
|
68
74
|
const entries = fs.readdirSync(localDir);
|
|
@@ -90,8 +96,9 @@ export const cloneAndStackPush = async (opts) => {
|
|
|
90
96
|
if (!(await hasStagedChanges(opts.localDir))) {
|
|
91
97
|
return { action, committed: false, commitSha: null };
|
|
92
98
|
}
|
|
99
|
+
const commitMessage = typeof opts.commitMessage === 'function' ? opts.commitMessage() : opts.commitMessage;
|
|
93
100
|
await spawnGit(['-C', opts.localDir, 'commit', '-F', '-'], {
|
|
94
|
-
stdin:
|
|
101
|
+
stdin: commitMessage,
|
|
95
102
|
env: {
|
|
96
103
|
GIT_AUTHOR_NAME: author.name,
|
|
97
104
|
GIT_AUTHOR_EMAIL: author.email,
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: "0.0.
|
|
1
|
+
declare const _default: "0.0.35";
|
|
2
2
|
export default _default;
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// AUTO-GENERATED from package.json by scripts/generate-version.mjs — do not edit.
|
|
2
|
-
export default '0.0.
|
|
2
|
+
export default '0.0.35';
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"packageManager": "yarn@4.11.0+sha512.4e54aeace9141df2f0177c266b05ec50dc044638157dae128c471ba65994ac802122d7ab35bcd9e81641228b7dcf24867d28e750e0bcae8a05277d600008ad54",
|
|
3
3
|
"name": "@quonfig/cli",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.35",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/quonfig/cli/issues"
|
|
7
7
|
},
|