@quonfig/cli 0.0.31 → 0.0.33
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 +40 -40
- package/dist/commands/migrate.js +34 -9
- package/dist/commands/pull.d.ts +11 -0
- package/dist/commands/pull.js +27 -11
- package/dist/commands/push.js +1 -0
- package/dist/commands/whoami.js +18 -43
- package/dist/migrate/local-write.d.ts +3 -1
- package/dist/migrate/local-write.js +12 -1
- package/dist/migrate/migrate-suggestion.d.ts +12 -0
- package/dist/migrate/migrate-suggestion.js +4 -0
- package/dist/migrate/migration-report.d.ts +7 -1
- package/dist/migrate/migration-report.js +24 -0
- package/dist/migrate/push-to-cloud.d.ts +14 -1
- package/dist/migrate/push-to-cloud.js +30 -1
- package/dist/migrate/source.d.ts +19 -0
- package/dist/migrate/sources/launch/translate.d.ts +1 -1
- package/dist/migrate/sources/launch/translate.js +45 -1
- package/dist/migrate/sources/launch.js +33 -1
- package/dist/push/run-push.d.ts +8 -0
- package/dist/push/run-push.js +16 -7
- package/dist/util/clone-and-stack-push.d.ts +3 -0
- package/dist/util/clone-and-stack-push.js +14 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/oclif.manifest.json +6 -6
- 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.33 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.33/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.33/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.33/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.33/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.33/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.33/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.33/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.33/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.33/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.33/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.33/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.33/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.33/src/commands/logout.ts)_
|
|
755
755
|
|
|
756
756
|
## `qfg migrate`
|
|
757
757
|
|
|
@@ -766,7 +766,7 @@ USAGE
|
|
|
766
766
|
FLAGS
|
|
767
767
|
--api-key=<value> API key for the legacy source (or set LAUNCH_API_KEY)
|
|
768
768
|
--dir=<value> Target local workspace directory. Defaults to cwd if it looks like a Quonfig workspace, otherwise
|
|
769
|
-
./quonfig-
|
|
769
|
+
./quonfig-repo.
|
|
770
770
|
--dry-run Fetch and summarize changes without writing anything
|
|
771
771
|
--from=<option> (required) Legacy source to migrate from
|
|
772
772
|
<options: launch|launchdarkly|flagsmith>
|
|
@@ -794,18 +794,18 @@ DESCRIPTION
|
|
|
794
794
|
and https://docs.quonfig.com/docs/migrating/troubleshooting when something goes sideways.
|
|
795
795
|
|
|
796
796
|
EXAMPLES
|
|
797
|
-
$ qfg migrate --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-
|
|
797
|
+
$ qfg migrate --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-repo
|
|
798
798
|
|
|
799
799
|
$ qfg migrate --from launch --api-key $LAUNCH_API_KEY --workspace acme-prod --push
|
|
800
800
|
|
|
801
|
-
$ qfg migrate --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-
|
|
801
|
+
$ qfg migrate --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-repo --reset
|
|
802
802
|
|
|
803
|
-
$ qfg migrate --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-
|
|
803
|
+
$ qfg migrate --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-repo --dry-run
|
|
804
804
|
|
|
805
|
-
$ qfg migrate --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-
|
|
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.33/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.33/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.33/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.33/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.33/src/commands/override.ts)_
|
|
965
965
|
|
|
966
966
|
## `qfg pull`
|
|
967
967
|
|
|
@@ -1005,7 +1005,7 @@ EXAMPLES
|
|
|
1005
1005
|
$ qfg pull # uses QUONFIG_DIR env var
|
|
1006
1006
|
```
|
|
1007
1007
|
|
|
1008
|
-
_See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1008
|
+
_See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.0.33/src/commands/pull.ts)_
|
|
1009
1009
|
|
|
1010
1010
|
## `qfg push`
|
|
1011
1011
|
|
|
@@ -1053,7 +1053,7 @@ EXAMPLES
|
|
|
1053
1053
|
$ qfg push --dir ./our-config --yes
|
|
1054
1054
|
```
|
|
1055
1055
|
|
|
1056
|
-
_See code: [src/commands/push.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1056
|
+
_See code: [src/commands/push.ts](https://github.com/quonfig/cli/blob/v0.0.33/src/commands/push.ts)_
|
|
1057
1057
|
|
|
1058
1058
|
## `qfg schema NAME`
|
|
1059
1059
|
|
|
@@ -1091,7 +1091,7 @@ EXAMPLES
|
|
|
1091
1091
|
$ qfg schema my-schema --set-json-schema=@schemas/my-schema.json --protected
|
|
1092
1092
|
```
|
|
1093
1093
|
|
|
1094
|
-
_See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1094
|
+
_See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.33/src/commands/schema.ts)_
|
|
1095
1095
|
|
|
1096
1096
|
## `qfg sdk-key`
|
|
1097
1097
|
|
|
@@ -1118,7 +1118,7 @@ EXAMPLES
|
|
|
1118
1118
|
$ qfg sdk-key revoke <key-id>
|
|
1119
1119
|
```
|
|
1120
1120
|
|
|
1121
|
-
_See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1121
|
+
_See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.0.33/src/commands/sdk-key.ts)_
|
|
1122
1122
|
|
|
1123
1123
|
## `qfg sdk-key create`
|
|
1124
1124
|
|
|
@@ -1150,7 +1150,7 @@ EXAMPLES
|
|
|
1150
1150
|
$ qfg sdk-key create --environment staging --type browser
|
|
1151
1151
|
```
|
|
1152
1152
|
|
|
1153
|
-
_See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1153
|
+
_See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.0.33/src/commands/sdk-key/create.ts)_
|
|
1154
1154
|
|
|
1155
1155
|
## `qfg sdk-key list`
|
|
1156
1156
|
|
|
@@ -1179,7 +1179,7 @@ EXAMPLES
|
|
|
1179
1179
|
$ qfg sdk-key list --environment production
|
|
1180
1180
|
```
|
|
1181
1181
|
|
|
1182
|
-
_See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1182
|
+
_See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.0.33/src/commands/sdk-key/list.ts)_
|
|
1183
1183
|
|
|
1184
1184
|
## `qfg sdk-key revoke KEYID`
|
|
1185
1185
|
|
|
@@ -1206,7 +1206,7 @@ EXAMPLES
|
|
|
1206
1206
|
$ qfg sdk-key revoke a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
|
1207
1207
|
```
|
|
1208
1208
|
|
|
1209
|
-
_See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1209
|
+
_See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.0.33/src/commands/sdk-key/revoke.ts)_
|
|
1210
1210
|
|
|
1211
1211
|
## `qfg set-default [NAME]`
|
|
1212
1212
|
|
|
@@ -1277,7 +1277,7 @@ EXAMPLES
|
|
|
1277
1277
|
$ qfg pull && qfg config-schema
|
|
1278
1278
|
```
|
|
1279
1279
|
|
|
1280
|
-
_See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1280
|
+
_See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.33/src/commands/set-default.ts)_
|
|
1281
1281
|
|
|
1282
1282
|
## `qfg set-rollout [NAME]`
|
|
1283
1283
|
|
|
@@ -1354,7 +1354,7 @@ EXAMPLES
|
|
|
1354
1354
|
$ qfg set-rollout my.variant.flag --environment production --weights "a:33,b:33,c:34" --hash-by user.id
|
|
1355
1355
|
```
|
|
1356
1356
|
|
|
1357
|
-
_See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1357
|
+
_See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.0.33/src/commands/set-rollout.ts)_
|
|
1358
1358
|
|
|
1359
1359
|
## `qfg sync`
|
|
1360
1360
|
|
|
@@ -1386,7 +1386,7 @@ EXAMPLES
|
|
|
1386
1386
|
$ qfg sync --watch --dir ./our-config --interval 30
|
|
1387
1387
|
```
|
|
1388
1388
|
|
|
1389
|
-
_See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1389
|
+
_See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.33/src/commands/sync.ts)_
|
|
1390
1390
|
|
|
1391
1391
|
## `qfg toggle [NAME]`
|
|
1392
1392
|
|
|
@@ -1488,7 +1488,7 @@ EXAMPLES
|
|
|
1488
1488
|
$ qfg verify --json
|
|
1489
1489
|
```
|
|
1490
1490
|
|
|
1491
|
-
_See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1491
|
+
_See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.33/src/commands/verify.ts)_
|
|
1492
1492
|
|
|
1493
1493
|
## `qfg whoami`
|
|
1494
1494
|
|
|
@@ -1511,7 +1511,7 @@ EXAMPLES
|
|
|
1511
1511
|
$ qfg whoami
|
|
1512
1512
|
```
|
|
1513
1513
|
|
|
1514
|
-
_See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1514
|
+
_See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.33/src/commands/whoami.ts)_
|
|
1515
1515
|
|
|
1516
1516
|
## `qfg workspace`
|
|
1517
1517
|
|
|
@@ -1534,7 +1534,7 @@ EXAMPLES
|
|
|
1534
1534
|
$ qfg workspace
|
|
1535
1535
|
```
|
|
1536
1536
|
|
|
1537
|
-
_See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1537
|
+
_See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.33/src/commands/workspace.ts)_
|
|
1538
1538
|
|
|
1539
1539
|
## `qfg workspace bootstrap`
|
|
1540
1540
|
|
|
@@ -1567,7 +1567,7 @@ EXAMPLES
|
|
|
1567
1567
|
$ qfg workspace bootstrap --dir ./our-config --skip-validate
|
|
1568
1568
|
```
|
|
1569
1569
|
|
|
1570
|
-
_See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1570
|
+
_See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.0.33/src/commands/workspace/bootstrap.ts)_
|
|
1571
1571
|
|
|
1572
1572
|
## `qfg workspace create SLUG`
|
|
1573
1573
|
|
|
@@ -1601,7 +1601,7 @@ EXAMPLES
|
|
|
1601
1601
|
$ qfg workspace create my-team --org 11111111-1111-1111-1111-111111111111
|
|
1602
1602
|
```
|
|
1603
1603
|
|
|
1604
|
-
_See code: [src/commands/workspace/create.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1604
|
+
_See code: [src/commands/workspace/create.ts](https://github.com/quonfig/cli/blob/v0.0.33/src/commands/workspace/create.ts)_
|
|
1605
1605
|
|
|
1606
1606
|
## `qfg workspace switch [SLUG]`
|
|
1607
1607
|
|
|
@@ -1629,7 +1629,7 @@ EXAMPLES
|
|
|
1629
1629
|
$ qfg workspace switch acme/production
|
|
1630
1630
|
```
|
|
1631
1631
|
|
|
1632
|
-
_See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1632
|
+
_See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.0.33/src/commands/workspace/switch.ts)_
|
|
1633
1633
|
<!-- commandsstop -->
|
|
1634
1634
|
|
|
1635
1635
|
## Local Development
|
package/dist/commands/migrate.js
CHANGED
|
@@ -4,8 +4,9 @@ import path from 'node:path';
|
|
|
4
4
|
import { BaseCommand } from '../index.js';
|
|
5
5
|
import { CrossSourceError, assertSourceMatches, readImportState } from '../migrate/import-state.js';
|
|
6
6
|
import { applyLocalMigration } from '../migrate/local-write.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { buildPushConflictSuggestion } from '../migrate/migrate-suggestion.js';
|
|
8
|
+
import { PushConflictError, PushHookRejectedError } from '../util/clone-and-stack-push.js';
|
|
9
|
+
import { MigratorVerifyError, pushMigrationToCloud } from '../migrate/push-to-cloud.js';
|
|
9
10
|
import { UnknownSourceError, getSource } from '../migrate/registry.js';
|
|
10
11
|
import { NotYetImplementedError, } from '../migrate/source.js';
|
|
11
12
|
import { applyLaunchBaseUrl } from '../migrate/sources/launch/api.js';
|
|
@@ -13,7 +14,7 @@ import { mintGiteaToken } from '../util/gitea-api.js';
|
|
|
13
14
|
import { displayUrl } from '../util/git-ops.js';
|
|
14
15
|
import { resolveWorkspaceUuid } from '../util/resolve-workspace.js';
|
|
15
16
|
import { loadAuthConfig } from '../util/token-storage.js';
|
|
16
|
-
const DEFAULT_DIR = 'quonfig-
|
|
17
|
+
const DEFAULT_DIR = 'quonfig-repo';
|
|
17
18
|
export default class Migrate extends BaseCommand {
|
|
18
19
|
static description = 'Migrate flags and configs from a legacy feature-flag system into a Quonfig workspace.\n\n' +
|
|
19
20
|
'Currently supported sources:\n' +
|
|
@@ -21,11 +22,11 @@ export default class Migrate extends BaseCommand {
|
|
|
21
22
|
'See https://docs.quonfig.com/docs/migrating/from-launch for the step-by-step guide,\n' +
|
|
22
23
|
'and https://docs.quonfig.com/docs/migrating/troubleshooting when something goes sideways.';
|
|
23
24
|
static examples = [
|
|
24
|
-
'<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-
|
|
25
|
+
'<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-repo',
|
|
25
26
|
'<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --workspace acme-prod --push',
|
|
26
|
-
'<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-
|
|
27
|
-
'<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-
|
|
28
|
-
'<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-
|
|
27
|
+
'<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-repo --reset',
|
|
28
|
+
'<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-repo --dry-run',
|
|
29
|
+
'<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-repo --staging',
|
|
29
30
|
];
|
|
30
31
|
static flags = {
|
|
31
32
|
'api-key': Flags.string({
|
|
@@ -33,7 +34,7 @@ export default class Migrate extends BaseCommand {
|
|
|
33
34
|
env: 'LAUNCH_API_KEY',
|
|
34
35
|
}),
|
|
35
36
|
dir: Flags.string({
|
|
36
|
-
description: 'Target local workspace directory. Defaults to cwd if it looks like a Quonfig workspace, otherwise ./quonfig-
|
|
37
|
+
description: 'Target local workspace directory. Defaults to cwd if it looks like a Quonfig workspace, otherwise ./quonfig-repo.',
|
|
37
38
|
}),
|
|
38
39
|
'dry-run': Flags.boolean({
|
|
39
40
|
default: false,
|
|
@@ -171,8 +172,16 @@ export default class Migrate extends BaseCommand {
|
|
|
171
172
|
};
|
|
172
173
|
}
|
|
173
174
|
catch (error) {
|
|
175
|
+
if (error instanceof PushHookRejectedError) {
|
|
176
|
+
return this.err(`${error.message}\n\nThe remote validation hook rejected the push. Fix the errors reported above (typically in the source system or by editing the local files in ${dir}), then re-run with --push. You can re-run \`qfg verify ${dir}\` locally to confirm the fixes before pushing.`);
|
|
177
|
+
}
|
|
174
178
|
if (error instanceof PushConflictError) {
|
|
175
|
-
return this.err(`${error.message}\n\
|
|
179
|
+
return this.err(`${error.message}\n\n${buildPushConflictSuggestion({ from: flags.from, userWorkspaceFlag: flags.workspace })}`);
|
|
180
|
+
}
|
|
181
|
+
if (error instanceof MigratorVerifyError) {
|
|
182
|
+
return this.err(`${error.message}\n\nThe migrated workspace was written to ${dir} but was NOT pushed. ` +
|
|
183
|
+
`Fix the issues above (typically in the source system or by editing the local files), then re-run with --push. ` +
|
|
184
|
+
`You can re-run \`qfg verify ${dir}\` locally to confirm the fixes before pushing.`);
|
|
176
185
|
}
|
|
177
186
|
throw error;
|
|
178
187
|
}
|
|
@@ -213,6 +222,7 @@ export default class Migrate extends BaseCommand {
|
|
|
213
222
|
// detectDuplicateKeys). The source accumulator holds the data regardless.
|
|
214
223
|
warnAboutDroppedOverrides(this, source.getDroppedOverrides?.() ?? null);
|
|
215
224
|
warnAboutSkippedConfigs(this, source.getSkippedConfigs?.() ?? null);
|
|
225
|
+
warnAboutCoercedSentinels(this, source.getCoercedSentinels?.() ?? null);
|
|
216
226
|
warnAboutDuplicateResolutions(this, duplicateResolutionsForWarn);
|
|
217
227
|
}
|
|
218
228
|
}
|
|
@@ -283,6 +293,21 @@ function warnAboutDuplicateResolutions(cmd, resolved) {
|
|
|
283
293
|
}
|
|
284
294
|
cmd.warn('Full detail is also written to MIGRATION_REPORT.md.');
|
|
285
295
|
}
|
|
296
|
+
function warnAboutCoercedSentinels(cmd, coerced) {
|
|
297
|
+
if (!coerced || coerced.total === 0)
|
|
298
|
+
return;
|
|
299
|
+
const envIds = Object.keys(coerced.byEnv).sort();
|
|
300
|
+
cmd.warn(`Coerced ${coerced.total} sentinel rule value(s) from Launch's "no value set yet" sentinel ({type:"string", value:""}) to the typed default for the surrounding config. Without coercion the qfg-verify hook would reject these as type-mismatches and fail-stop the entire push:`);
|
|
301
|
+
for (const envId of envIds) {
|
|
302
|
+
const perFlag = coerced.byEnv[envId];
|
|
303
|
+
const totalForEnv = Object.values(perFlag).reduce((s, n) => s + n, 0);
|
|
304
|
+
cmd.warn(` env-${envId}: ${totalForEnv} coerced from ${Object.keys(perFlag).length} config(s)`);
|
|
305
|
+
for (const flagPath of Object.keys(perFlag).sort()) {
|
|
306
|
+
cmd.warn(` - ${flagPath} (${perFlag[flagPath]})`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
cmd.warn(`If you want a real default for these rules, set one in the source system and re-run the migration. Full detail is also written to MIGRATION_REPORT.md.`);
|
|
310
|
+
}
|
|
286
311
|
function warnAboutSkippedConfigs(cmd, skipped) {
|
|
287
312
|
if (!skipped || skipped.total === 0)
|
|
288
313
|
return;
|
package/dist/commands/pull.d.ts
CHANGED
|
@@ -8,6 +8,17 @@ export default class Pull extends BaseCommand {
|
|
|
8
8
|
workspace: import("@oclif/core/lib/interfaces/parser.js").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces/parser.js").CustomOptions>;
|
|
9
9
|
};
|
|
10
10
|
run(): Promise<JsonObj | void>;
|
|
11
|
+
/**
|
|
12
|
+
* Wraps `readWorkspaceSlug` so a legacy bare-slug value (`"workspace":
|
|
13
|
+
* "foo"` instead of `"workspace": "org/foo"`) reads as `undefined`
|
|
14
|
+
* rather than throwing. The caller then takes the "no pin set" branch
|
|
15
|
+
* and overwrites with the canonical form, completing the migration.
|
|
16
|
+
*
|
|
17
|
+
* Any other parse error still resolves to `undefined` here (and is
|
|
18
|
+
* verbose-logged) — backfill is non-fatal; the next push surfaces the
|
|
19
|
+
* real diagnosis.
|
|
20
|
+
*/
|
|
21
|
+
private readPinTolerant;
|
|
11
22
|
/**
|
|
12
23
|
* Local-only backfill of the `workspace` pin in `quonfig.json`.
|
|
13
24
|
*
|
package/dist/commands/pull.js
CHANGED
|
@@ -134,6 +134,25 @@ CLI shortcuts (no JSON editing needed for simple cases):
|
|
|
134
134
|
await this.backfillWorkspacePin(resolvedDir, tokenEntry, workspaceId, orgSlug);
|
|
135
135
|
return { dir: resolvedDir, workspaceId };
|
|
136
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Wraps `readWorkspaceSlug` so a legacy bare-slug value (`"workspace":
|
|
139
|
+
* "foo"` instead of `"workspace": "org/foo"`) reads as `undefined`
|
|
140
|
+
* rather than throwing. The caller then takes the "no pin set" branch
|
|
141
|
+
* and overwrites with the canonical form, completing the migration.
|
|
142
|
+
*
|
|
143
|
+
* Any other parse error still resolves to `undefined` here (and is
|
|
144
|
+
* verbose-logged) — backfill is non-fatal; the next push surfaces the
|
|
145
|
+
* real diagnosis.
|
|
146
|
+
*/
|
|
147
|
+
async readPinTolerant(dir) {
|
|
148
|
+
try {
|
|
149
|
+
return await readWorkspaceSlug(dir);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
this.verboseLog('Pull', `quonfig.json workspace pin needs migration: ${String(error)}`);
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
137
156
|
/**
|
|
138
157
|
* Local-only backfill of the `workspace` pin in `quonfig.json`.
|
|
139
158
|
*
|
|
@@ -165,18 +184,15 @@ CLI shortcuts (no JSON editing needed for simple cases):
|
|
|
165
184
|
return;
|
|
166
185
|
}
|
|
167
186
|
// The pin is stored as `<org-slug>/<workspace-slug>`. The backend's
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
this.verboseLog('Pull', `Backend workspaceSlug "${backendSlug}" is not in <org>/<ws> form; skipping pin backfill.`);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
187
|
+
// mint-token still returns the bare workspace component; we already
|
|
188
|
+
// resolved the org separately during workspace lookup, so combine the
|
|
189
|
+
// two halves locally. If the backend ever upgrades to returning slash
|
|
190
|
+
// form, tryParseWorkspacePin handles it; the explicit org overrides
|
|
191
|
+
// any mismatch in favor of the workspace we actually authenticated as.
|
|
192
|
+
const parsedBackend = tryParseWorkspacePin(backendSlug);
|
|
193
|
+
const backendPin = parsedBackend ?? { orgSlug, workspaceSlug: backendSlug };
|
|
178
194
|
try {
|
|
179
|
-
const existingPin = await
|
|
195
|
+
const existingPin = await this.readPinTolerant(dir);
|
|
180
196
|
if (!existingPin) {
|
|
181
197
|
await writeWorkspaceSlug(dir, backendPin);
|
|
182
198
|
const formatted = `${backendPin.orgSlug}/${backendPin.workspaceSlug}`;
|
package/dist/commands/push.js
CHANGED
package/dist/commands/whoami.js
CHANGED
|
@@ -26,13 +26,15 @@ export default class Whoami extends BaseCommand {
|
|
|
26
26
|
}
|
|
27
27
|
const activeOrgId = await this.resolveActiveOrgId(store.defaultOrgId);
|
|
28
28
|
const memberships = await this.fetchOrganizations();
|
|
29
|
-
const localOrgs = Object.entries(store.tokensByOrg).map(([orgId, t]) =>
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
const localOrgs = Object.entries(store.tokensByOrg).map(([orgId, t]) => {
|
|
30
|
+
this.verboseLog('whoami: token expiry', { orgSlug: t.org_slug ?? orgId, ...describeExpiry(t) });
|
|
31
|
+
return {
|
|
32
|
+
workosOrgId: orgId,
|
|
33
|
+
slug: t.org_slug ?? orgId,
|
|
34
|
+
name: t.org_name ?? t.org_slug ?? orgId,
|
|
35
|
+
role: '—',
|
|
36
|
+
};
|
|
37
|
+
});
|
|
36
38
|
const merged = mergeMemberships(localOrgs, memberships);
|
|
37
39
|
this.log(`Logged in as: ${userEmail || 'Unknown'}`);
|
|
38
40
|
this.log('Orgs:');
|
|
@@ -45,13 +47,7 @@ export default class Whoami extends BaseCommand {
|
|
|
45
47
|
const marker = org.workosOrgId === activeOrgId ? '*' : ' ';
|
|
46
48
|
const label = `${org.slug}${org.workosOrgId === activeOrgId ? ' (active)' : ''}`;
|
|
47
49
|
const padded = label.padEnd(labelWidth, ' ');
|
|
48
|
-
|
|
49
|
-
this.log(` ${marker} ${padded}— ${org.role}${statusSuffix}`);
|
|
50
|
-
}
|
|
51
|
-
const expiredCount = merged.filter((o) => o.sessionStatus?.expired).length;
|
|
52
|
-
if (expiredCount > 0) {
|
|
53
|
-
this.log('');
|
|
54
|
-
this.log(`${expiredCount} of ${merged.length} session(s) expired. Run \`qfg login\` to refresh.`);
|
|
50
|
+
this.log(` ${marker} ${padded}— ${org.role}`);
|
|
55
51
|
}
|
|
56
52
|
}
|
|
57
53
|
return {
|
|
@@ -65,7 +61,6 @@ export default class Whoami extends BaseCommand {
|
|
|
65
61
|
name: o.name,
|
|
66
62
|
role: o.role,
|
|
67
63
|
active: o.workosOrgId === activeOrgId,
|
|
68
|
-
sessionExpired: o.sessionStatus?.expired ?? null,
|
|
69
64
|
})),
|
|
70
65
|
};
|
|
71
66
|
}
|
|
@@ -147,21 +142,17 @@ function mergeMemberships(local, server) {
|
|
|
147
142
|
const merged = new Map();
|
|
148
143
|
for (const o of local)
|
|
149
144
|
merged.set(o.workosOrgId, o);
|
|
150
|
-
for (const o of server)
|
|
151
|
-
|
|
152
|
-
// a property of the locally-cached token, so preserve it.
|
|
153
|
-
const existing = merged.get(o.workosOrgId);
|
|
154
|
-
merged.set(o.workosOrgId, { ...o, sessionStatus: existing?.sessionStatus });
|
|
155
|
-
}
|
|
145
|
+
for (const o of server)
|
|
146
|
+
merged.set(o.workosOrgId, o);
|
|
156
147
|
return [...merged.values()].sort((a, b) => a.slug.localeCompare(b.slug));
|
|
157
148
|
}
|
|
158
149
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
150
|
+
* Verbose-only diagnostic: reports the access token's remaining lifetime.
|
|
151
|
+
* Access tokens are short-lived (~5 min) and `getValidAccessToken` refreshes
|
|
152
|
+
* them transparently on next use, so this is informational, not actionable.
|
|
153
|
+
* The hot stuff — actual refresh failures — is logged from get-valid-token.
|
|
163
154
|
*/
|
|
164
|
-
function
|
|
155
|
+
function describeExpiry(tokens) {
|
|
165
156
|
let expMs = tokens.expires_at;
|
|
166
157
|
try {
|
|
167
158
|
const payload = decodeJWT(tokens.access_token);
|
|
@@ -172,23 +163,7 @@ function sessionStatusFor(tokens) {
|
|
|
172
163
|
/* keep stored expires_at */
|
|
173
164
|
}
|
|
174
165
|
const delta = expMs - Date.now();
|
|
175
|
-
|
|
176
|
-
return { expired: true, description: 'expired — run `qfg login`' };
|
|
177
|
-
return { expired: false, description: `expires in ${formatRelative(delta)}` };
|
|
178
|
-
}
|
|
179
|
-
function formatRelative(deltaMs) {
|
|
180
|
-
const minutes = Math.round(deltaMs / 60_000);
|
|
181
|
-
if (minutes < 1)
|
|
182
|
-
return '<1m';
|
|
183
|
-
if (minutes < 60)
|
|
184
|
-
return `${minutes}m`;
|
|
185
|
-
const hours = Math.floor(minutes / 60);
|
|
186
|
-
const remMin = minutes % 60;
|
|
187
|
-
if (hours < 24)
|
|
188
|
-
return remMin > 0 ? `${hours}h ${remMin}m` : `${hours}h`;
|
|
189
|
-
const days = Math.floor(hours / 24);
|
|
190
|
-
const remHours = hours % 24;
|
|
191
|
-
return remHours > 0 ? `${days}d ${remHours}h` : `${days}d`;
|
|
166
|
+
return { expired: delta <= 0, expiresAt: expMs, deltaMs: delta };
|
|
192
167
|
}
|
|
193
168
|
function pickAnyOrgId(store) {
|
|
194
169
|
if (store.defaultOrgId && store.tokensByOrg[store.defaultOrgId])
|
|
@@ -1,7 +1,7 @@
|
|
|
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 { DroppedOverrideSummary, DuplicateResolutionSummary, LegacyChange, MigrationSource, SkippedConfigSummary } from './source.js';
|
|
4
|
+
import type { CoercedSentinelSummary, DroppedOverrideSummary, 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;
|
|
@@ -19,6 +19,8 @@ export interface ApplyLocalMigrationOptions {
|
|
|
19
19
|
export interface ApplyLocalMigrationResult {
|
|
20
20
|
/** `'initialized'` if we had to `git init` the target, `'reused'` if it was already a repo. */
|
|
21
21
|
action: 'initialized' | 'reused';
|
|
22
|
+
/** Sentinel rule values coerced to typed defaults during translate. Null if none. */
|
|
23
|
+
coercedSentinels: CoercedSentinelSummary | null;
|
|
22
24
|
commitSha: string | null;
|
|
23
25
|
/** `false` if translate produced no net changes — nothing was committed. */
|
|
24
26
|
committed: boolean;
|
|
@@ -85,9 +85,11 @@ export const applyLocalMigration = async (opts) => {
|
|
|
85
85
|
writeImportState(opts.localDir, opts.importState);
|
|
86
86
|
const droppedOverrides = opts.source.getDroppedOverrides?.() ?? null;
|
|
87
87
|
const skippedConfigs = opts.source.getSkippedConfigs?.() ?? null;
|
|
88
|
+
const coercedSentinels = opts.source.getCoercedSentinels?.() ?? null;
|
|
88
89
|
const duplicateResolutions = resolutionEntries.length > 0 ? { entries: resolutionEntries, total: resolutionEntries.length } : null;
|
|
89
90
|
const reportData = {
|
|
90
91
|
...opts.reportData,
|
|
92
|
+
...(coercedSentinels ? { coercedSentinels } : {}),
|
|
91
93
|
...(droppedOverrides ? { droppedOverrides } : {}),
|
|
92
94
|
...(duplicateResolutions ? { duplicateResolutions } : {}),
|
|
93
95
|
...(skippedConfigs ? { skippedConfigs } : {}),
|
|
@@ -95,7 +97,15 @@ export const applyLocalMigration = async (opts) => {
|
|
|
95
97
|
writeMigrationReport(opts.localDir, reportData);
|
|
96
98
|
await runGit(['-C', opts.localDir, 'add', '--all']);
|
|
97
99
|
if (!(await hasStagedChanges(opts.localDir))) {
|
|
98
|
-
return {
|
|
100
|
+
return {
|
|
101
|
+
action,
|
|
102
|
+
coercedSentinels,
|
|
103
|
+
committed: false,
|
|
104
|
+
commitSha: null,
|
|
105
|
+
droppedOverrides,
|
|
106
|
+
duplicateResolutions,
|
|
107
|
+
skippedConfigs,
|
|
108
|
+
};
|
|
99
109
|
}
|
|
100
110
|
await spawnGit(['-C', opts.localDir, 'commit', '-F', '-'], {
|
|
101
111
|
stdin: opts.commitMessage,
|
|
@@ -109,6 +119,7 @@ export const applyLocalMigration = async (opts) => {
|
|
|
109
119
|
const { stdout: sha } = await runGit(['-C', opts.localDir, 'rev-parse', 'HEAD']);
|
|
110
120
|
return {
|
|
111
121
|
action,
|
|
122
|
+
coercedSentinels,
|
|
112
123
|
commitSha: sha.trim(),
|
|
113
124
|
committed: true,
|
|
114
125
|
droppedOverrides,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface PushConflictSuggestionInput {
|
|
2
|
+
from: string;
|
|
3
|
+
/**
|
|
4
|
+
* The original `--workspace` flag string the user typed, e.g.
|
|
5
|
+
* `test-organization/semgrep-test-1`. Pass `undefined` if the user did not
|
|
6
|
+
* pass `--workspace` (and the command resolved the workspace via the saved
|
|
7
|
+
* profile). Never pass the resolved workspace UUID — the suggestion is meant
|
|
8
|
+
* to be a copy-paste of the user's original invocation.
|
|
9
|
+
*/
|
|
10
|
+
userWorkspaceFlag: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
export declare function buildPushConflictSuggestion({ from, userWorkspaceFlag }: PushConflictSuggestionInput): string;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export function buildPushConflictSuggestion({ from, userWorkspaceFlag }) {
|
|
2
|
+
const workspaceArg = userWorkspaceFlag ? ` --workspace ${userWorkspaceFlag}` : '';
|
|
3
|
+
return `Re-run \`qfg migrate --from ${from}${workspaceArg} --push\` to pick up remote changes before retrying.`;
|
|
4
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IdentifierMap } from './identifier-map.js';
|
|
2
|
-
import type { DroppedOverrideSummary, DuplicateResolutionSummary, SkippedConfigSummary } from './source.js';
|
|
2
|
+
import type { CoercedSentinelSummary, DroppedOverrideSummary, DuplicateResolutionSummary, SkippedConfigSummary } from './source.js';
|
|
3
3
|
export interface MigrationReportCounts {
|
|
4
4
|
environmentsMapped: number;
|
|
5
5
|
flagsMigrated: number;
|
|
@@ -29,6 +29,12 @@ export interface FollowUpChecklist {
|
|
|
29
29
|
}
|
|
30
30
|
export interface MigrationReportData {
|
|
31
31
|
cleanMappings: CleanMapping[];
|
|
32
|
+
/**
|
|
33
|
+
* Rule values that translate() coerced from a sentinel like Launch's
|
|
34
|
+
* empty-string "no value set yet" to the typed default. Null when nothing
|
|
35
|
+
* was coerced.
|
|
36
|
+
*/
|
|
37
|
+
coercedSentinels?: CoercedSentinelSummary | null;
|
|
32
38
|
counts: MigrationReportCounts;
|
|
33
39
|
/**
|
|
34
40
|
* Override sections that were dropped during translate() because the env.id was not
|
|
@@ -87,6 +87,27 @@ const renderDuplicateResolutions = (resolved) => {
|
|
|
87
87
|
}
|
|
88
88
|
return lines.join('\n');
|
|
89
89
|
};
|
|
90
|
+
const renderCoercedSentinels = (coerced) => {
|
|
91
|
+
if (!coerced || coerced.total === 0)
|
|
92
|
+
return null;
|
|
93
|
+
const envIds = Object.keys(coerced.byEnv).sort();
|
|
94
|
+
const lines = [
|
|
95
|
+
'## Coerced sentinel rule values',
|
|
96
|
+
'',
|
|
97
|
+
`Coerced **${coerced.total}** rule value(s) from Launch's "no value set yet" sentinel ({type:"string", value:""}) to the typed default for the surrounding config. The qfg-verify hook would otherwise reject these as type-mismatches and fail-stop the entire push. Affected envs: **${envIds.length}**. Review each and set a real default in the source system if needed.`,
|
|
98
|
+
];
|
|
99
|
+
for (const envId of envIds) {
|
|
100
|
+
const perFlag = coerced.byEnv[envId];
|
|
101
|
+
const totalForEnv = Object.values(perFlag).reduce((s, n) => s + n, 0);
|
|
102
|
+
const flagCount = Object.keys(perFlag).length;
|
|
103
|
+
lines.push('', `### env-${envId} — ${totalForEnv} coerced from ${flagCount} config(s)`, '');
|
|
104
|
+
const sorted = Object.keys(perFlag).sort();
|
|
105
|
+
for (const flagPath of sorted) {
|
|
106
|
+
lines.push(`- \`${flagPath}\` (${perFlag[flagPath]})`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return lines.join('\n');
|
|
110
|
+
};
|
|
90
111
|
const renderSkippedConfigs = (skipped) => {
|
|
91
112
|
if (!skipped || skipped.total === 0)
|
|
92
113
|
return null;
|
|
@@ -136,6 +157,9 @@ export const buildMigrationReport = (data) => {
|
|
|
136
157
|
const skipped = renderSkippedConfigs(data.skippedConfigs);
|
|
137
158
|
if (skipped !== null)
|
|
138
159
|
sections.push(skipped);
|
|
160
|
+
const coerced = renderCoercedSentinels(data.coercedSentinels);
|
|
161
|
+
if (coerced !== null)
|
|
162
|
+
sections.push(coerced);
|
|
139
163
|
sections.push(renderFollowUp(data.followUp));
|
|
140
164
|
return sections.join('\n\n') + '\n';
|
|
141
165
|
};
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { type ImportState } from './import-state.js';
|
|
2
2
|
import { type MigrationReportData } from './migration-report.js';
|
|
3
3
|
import { type CloneAndStackPushResult } from '../util/clone-and-stack-push.js';
|
|
4
|
-
import
|
|
4
|
+
import { type ValidationResult } from '../verify/validate.js';
|
|
5
|
+
import type { CoercedSentinelSummary, DroppedOverrideSummary, DuplicateResolutionSummary, LegacyChange, MigrationSource, SkippedConfigSummary } from './source.js';
|
|
6
|
+
/**
|
|
7
|
+
* Thrown when the migrator's freshly-written workspace fails the same checks
|
|
8
|
+
* that the Gitea pre-receive `qfg-verify` hook will run server-side. We raise
|
|
9
|
+
* this client-side after `applyDelta` writes files but before commit + push,
|
|
10
|
+
* so the customer sees the failure locally rather than after a full push
|
|
11
|
+
* round-trip is rejected (qfg-52qg).
|
|
12
|
+
*/
|
|
13
|
+
export declare class MigratorVerifyError extends Error {
|
|
14
|
+
readonly result: ValidationResult;
|
|
15
|
+
constructor(result: ValidationResult);
|
|
16
|
+
}
|
|
5
17
|
export interface PushMigrationToCloudOptions {
|
|
6
18
|
branch?: string;
|
|
7
19
|
changes: LegacyChange[];
|
|
@@ -19,6 +31,7 @@ export interface PushMigrationToCloudOptions {
|
|
|
19
31
|
source: MigrationSource;
|
|
20
32
|
}
|
|
21
33
|
export interface PushMigrationToCloudResult extends CloneAndStackPushResult {
|
|
34
|
+
coercedSentinels: CoercedSentinelSummary | null;
|
|
22
35
|
droppedOverrides: DroppedOverrideSummary | null;
|
|
23
36
|
duplicateResolutions: DuplicateResolutionSummary | null;
|
|
24
37
|
skippedConfigs: SkippedConfigSummary | null;
|
|
@@ -4,6 +4,25 @@ import { removeQfFromGitignore, writeImportState } from './import-state.js';
|
|
|
4
4
|
import { writeMigrationReport } from './migration-report.js';
|
|
5
5
|
import { detectDuplicateKeys } from './sources/launch/translate.js';
|
|
6
6
|
import { cloneAndStackPush, } from '../util/clone-and-stack-push.js';
|
|
7
|
+
import { formatResult, validateWorkspace } from '../verify/validate.js';
|
|
8
|
+
/**
|
|
9
|
+
* Thrown when the migrator's freshly-written workspace fails the same checks
|
|
10
|
+
* that the Gitea pre-receive `qfg-verify` hook will run server-side. We raise
|
|
11
|
+
* this client-side after `applyDelta` writes files but before commit + push,
|
|
12
|
+
* so the customer sees the failure locally rather than after a full push
|
|
13
|
+
* round-trip is rejected (qfg-52qg).
|
|
14
|
+
*/
|
|
15
|
+
export class MigratorVerifyError extends Error {
|
|
16
|
+
result;
|
|
17
|
+
constructor(result) {
|
|
18
|
+
const errorCount = result.issues.filter((i) => i.severity === 'error').length;
|
|
19
|
+
super(`Migrator output failed qfg verify: ${errorCount} error(s) in ${result.filesChecked} file(s). ` +
|
|
20
|
+
`Refusing to push — the same errors would be rejected by the server pre-receive hook.\n\n` +
|
|
21
|
+
formatResult(result));
|
|
22
|
+
this.name = 'MigratorVerifyError';
|
|
23
|
+
this.result = result;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
7
26
|
const writeQuonfigFiles = (dir, changes, source) => {
|
|
8
27
|
const livePaths = new Map();
|
|
9
28
|
for (const change of changes) {
|
|
@@ -65,6 +84,7 @@ const mergeEnvironmentsIntoQuonfigJson = (dir, sourceEnvs) => {
|
|
|
65
84
|
fs.writeFileSync(quonfigPath, JSON.stringify({ ...existing, environments: merged }, null, 2) + '\n', 'utf8');
|
|
66
85
|
};
|
|
67
86
|
export const pushMigrationToCloud = async (opts) => {
|
|
87
|
+
let coercedSentinels = null;
|
|
68
88
|
let droppedOverrides = null;
|
|
69
89
|
let duplicateResolutions = null;
|
|
70
90
|
let skippedConfigs = null;
|
|
@@ -78,15 +98,24 @@ export const pushMigrationToCloud = async (opts) => {
|
|
|
78
98
|
writeImportState(dir, opts.importState);
|
|
79
99
|
droppedOverrides = opts.source.getDroppedOverrides?.() ?? null;
|
|
80
100
|
skippedConfigs = opts.source.getSkippedConfigs?.() ?? null;
|
|
101
|
+
coercedSentinels = opts.source.getCoercedSentinels?.() ?? null;
|
|
81
102
|
duplicateResolutions =
|
|
82
103
|
resolutionEntries.length > 0 ? { entries: resolutionEntries, total: resolutionEntries.length } : null;
|
|
83
104
|
const reportData = {
|
|
84
105
|
...opts.reportData,
|
|
106
|
+
...(coercedSentinels ? { coercedSentinels } : {}),
|
|
85
107
|
...(droppedOverrides ? { droppedOverrides } : {}),
|
|
86
108
|
...(duplicateResolutions ? { duplicateResolutions } : {}),
|
|
87
109
|
...(skippedConfigs ? { skippedConfigs } : {}),
|
|
88
110
|
};
|
|
89
111
|
writeMigrationReport(dir, reportData);
|
|
112
|
+
// Pre-flight: run the same validation the Gitea pre-receive `qfg-verify`
|
|
113
|
+
// hook will run after the push lands. Failing here saves a full
|
|
114
|
+
// git-push round-trip and gives the customer a local, fixable error
|
|
115
|
+
// (qfg-52qg).
|
|
116
|
+
const verifyResult = validateWorkspace(dir);
|
|
117
|
+
if (!verifyResult.valid)
|
|
118
|
+
throw new MigratorVerifyError(verifyResult);
|
|
90
119
|
},
|
|
91
120
|
commitMessage: opts.commitMessage,
|
|
92
121
|
localDir: opts.localDir,
|
|
@@ -95,5 +124,5 @@ export const pushMigrationToCloud = async (opts) => {
|
|
|
95
124
|
if (opts.branch !== undefined)
|
|
96
125
|
cloneOpts.branch = opts.branch;
|
|
97
126
|
const result = await cloneAndStackPush(cloneOpts);
|
|
98
|
-
return { ...result, droppedOverrides, duplicateResolutions, skippedConfigs };
|
|
127
|
+
return { ...result, coercedSentinels, droppedOverrides, duplicateResolutions, skippedConfigs };
|
|
99
128
|
};
|
package/dist/migrate/source.d.ts
CHANGED
|
@@ -17,6 +17,19 @@ export interface DroppedOverrideSummary {
|
|
|
17
17
|
/** Total override sections dropped across every env + flag. */
|
|
18
18
|
total: number;
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* qfg-gpnd: Per-config tally of rule values that were coerced from a Launch
|
|
22
|
+
* "no value set yet" sentinel ({type:'string', value:''}) to the typed default
|
|
23
|
+
* because the surrounding config valueType was non-string. Tracking is per
|
|
24
|
+
* envId (or 'default' for the default section) so customers can see which
|
|
25
|
+
* environments were affected.
|
|
26
|
+
*/
|
|
27
|
+
export interface CoercedSentinelSummary {
|
|
28
|
+
/** Per-envId → per-output-path count of coerced sentinel rule values. */
|
|
29
|
+
byEnv: Record<string, Record<string, number>>;
|
|
30
|
+
/** Total rule values coerced across every env + config. */
|
|
31
|
+
total: number;
|
|
32
|
+
}
|
|
20
33
|
export interface SkippedConfigEntry {
|
|
21
34
|
/** Source key (pre-translate) that was skipped. */
|
|
22
35
|
key: string;
|
|
@@ -45,6 +58,12 @@ export interface DuplicateResolutionSummary {
|
|
|
45
58
|
}
|
|
46
59
|
export interface MigrationSource {
|
|
47
60
|
fetchChanges(sinceEpochMs: null | number): AsyncIterable<LegacyChange>;
|
|
61
|
+
/**
|
|
62
|
+
* Optional post-translate accumulator. Returns any rule values that translate()
|
|
63
|
+
* coerced from a sentinel like Launch's empty-string "no value set yet" to the
|
|
64
|
+
* typed default. Null when nothing was coerced.
|
|
65
|
+
*/
|
|
66
|
+
getCoercedSentinels?(): CoercedSentinelSummary | null;
|
|
48
67
|
/**
|
|
49
68
|
* Optional post-translate accumulator. Returns any override sections that were
|
|
50
69
|
* dropped during translate() calls because the env.id was not present in the
|
|
@@ -11,7 +11,7 @@ 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
|
-
export declare function transformConfig(config: LaunchConfig, envIdMap: Record<string, string>, onDroppedEnv?: (envId: string) => void): Record<string, unknown>;
|
|
14
|
+
export declare function transformConfig(config: LaunchConfig, envIdMap: Record<string, string>, onDroppedEnv?: (envId: string) => void, onCoercedSentinel?: (envId: string, valueType: string) => void): Record<string, unknown>;
|
|
15
15
|
export declare function getOutputPath(type: string, key: string): string;
|
|
16
16
|
/**
|
|
17
17
|
* Walks the set of written files looking for cross-type collisions (the same
|
|
@@ -102,6 +102,30 @@ function normalizeRules(rules, configKey, path) {
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
// qfg-gpnd: Launch emits {type:'string', value:''} as a "no value set yet"
|
|
106
|
+
// sentinel even when the surrounding config valueType is non-string (most
|
|
107
|
+
// commonly seen on catch-all/ALWAYS_TRUE rules). The qfg-verify hook rejects
|
|
108
|
+
// these as type-mismatches and fail-stops the entire push. Coerce the sentinel
|
|
109
|
+
// to the typed default so the otherwise-valid config still ships, and surface
|
|
110
|
+
// each coercion via the callback so the migrator can report it.
|
|
111
|
+
function coerceSentinelRules(rules, valueType, onCoerced) {
|
|
112
|
+
if (!Array.isArray(rules))
|
|
113
|
+
return;
|
|
114
|
+
if (valueType === 'string')
|
|
115
|
+
return;
|
|
116
|
+
for (const rule of rules) {
|
|
117
|
+
if (!rule || typeof rule !== 'object')
|
|
118
|
+
continue;
|
|
119
|
+
const r = rule;
|
|
120
|
+
const v = r.value;
|
|
121
|
+
if (!v || typeof v !== 'object')
|
|
122
|
+
continue;
|
|
123
|
+
if (v.type === 'string' && v.value === '') {
|
|
124
|
+
r.value = zeroValue(valueType);
|
|
125
|
+
onCoerced();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
105
129
|
function normalizeJsonValuesInConfig(out) {
|
|
106
130
|
const configKey = String(out.key ?? 'unknown');
|
|
107
131
|
const defaultSection = out.default;
|
|
@@ -122,7 +146,7 @@ function normalizeJsonValuesInConfig(out) {
|
|
|
122
146
|
}
|
|
123
147
|
}
|
|
124
148
|
}
|
|
125
|
-
export function transformConfig(config, envIdMap, onDroppedEnv) {
|
|
149
|
+
export function transformConfig(config, envIdMap, onDroppedEnv, onCoercedSentinel) {
|
|
126
150
|
const out = JSON.parse(JSON.stringify(config));
|
|
127
151
|
delete out.changedBy;
|
|
128
152
|
delete out.modifiedAt;
|
|
@@ -137,6 +161,13 @@ export function transformConfig(config, envIdMap, onDroppedEnv) {
|
|
|
137
161
|
if (typeof out.valueType === 'string' && out.valueType.toUpperCase() === 'LOG_LEVEL_V2') {
|
|
138
162
|
out.valueType = 'log_level';
|
|
139
163
|
}
|
|
164
|
+
// qfg-ol8y: Launch passes through `valueType: 'not_set_value_type'` for
|
|
165
|
+
// segments where the field was never explicitly set. qfg-verify rejects
|
|
166
|
+
// that since segments must have valueType 'bool'. Segments in qfg are
|
|
167
|
+
// unconditionally boolean ("is the user in the segment?"), so force-set.
|
|
168
|
+
if (typeof out.type === 'string' && out.type.toLowerCase() === 'segment') {
|
|
169
|
+
out.valueType = 'bool';
|
|
170
|
+
}
|
|
140
171
|
if (out.type === 'log_level' && typeof out.key === 'string') {
|
|
141
172
|
out.key = normalizeLogLevelKey(out.key);
|
|
142
173
|
}
|
|
@@ -199,6 +230,19 @@ export function transformConfig(config, envIdMap, onDroppedEnv) {
|
|
|
199
230
|
],
|
|
200
231
|
};
|
|
201
232
|
}
|
|
233
|
+
if (typeof out.valueType === 'string') {
|
|
234
|
+
const valueType = out.valueType;
|
|
235
|
+
const defaultSection = out.default;
|
|
236
|
+
if (defaultSection && Array.isArray(defaultSection.rules)) {
|
|
237
|
+
coerceSentinelRules(defaultSection.rules, valueType, () => onCoercedSentinel?.('default', valueType));
|
|
238
|
+
}
|
|
239
|
+
if (Array.isArray(out.environments)) {
|
|
240
|
+
for (const env of out.environments) {
|
|
241
|
+
const envId = typeof env.id === 'string' ? env.id : 'unknown';
|
|
242
|
+
coerceSentinelRules(env.rules, valueType, () => onCoercedSentinel?.(envId, valueType));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
202
246
|
normalizeJsonValuesInConfig(out);
|
|
203
247
|
return out;
|
|
204
248
|
}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { fetchAllChangeHistory, fetchEnvironments } from './launch/api.js';
|
|
2
2
|
import { InvalidSourceConfigError, getOutputPath, isLegacyLogLevel, slugify, transformConfig, } from './launch/translate.js';
|
|
3
3
|
const SOURCE_NAME = 'launch';
|
|
4
|
-
const state = {
|
|
4
|
+
const state = {
|
|
5
|
+
apiKey: null,
|
|
6
|
+
coercedSentinels: new Map(),
|
|
7
|
+
droppedOverrides: new Map(),
|
|
8
|
+
envIdMap: null,
|
|
9
|
+
skippedConfigs: [],
|
|
10
|
+
};
|
|
5
11
|
class MissingAuthError extends Error {
|
|
6
12
|
constructor(operation) {
|
|
7
13
|
super(`launch source ${operation} requires validateAuth(apiKey) to be called first (no API key configured).`);
|
|
@@ -58,6 +64,13 @@ function translateImpl(change) {
|
|
|
58
64
|
state.droppedOverrides.set(envId, perFlag);
|
|
59
65
|
}
|
|
60
66
|
perFlag.set(outputPath, (perFlag.get(outputPath) ?? 0) + 1);
|
|
67
|
+
}, (envId) => {
|
|
68
|
+
let perFlag = state.coercedSentinels.get(envId);
|
|
69
|
+
if (!perFlag) {
|
|
70
|
+
perFlag = new Map();
|
|
71
|
+
state.coercedSentinels.set(envId, perFlag);
|
|
72
|
+
}
|
|
73
|
+
perFlag.set(outputPath, (perFlag.get(outputPath) ?? 0) + 1);
|
|
61
74
|
});
|
|
62
75
|
}
|
|
63
76
|
catch (error) {
|
|
@@ -85,6 +98,22 @@ function getDroppedOverridesImpl() {
|
|
|
85
98
|
}
|
|
86
99
|
return { byEnv, total };
|
|
87
100
|
}
|
|
101
|
+
function getCoercedSentinelsImpl() {
|
|
102
|
+
if (state.coercedSentinels.size === 0)
|
|
103
|
+
return null;
|
|
104
|
+
const byEnv = {};
|
|
105
|
+
let total = 0;
|
|
106
|
+
for (const envId of [...state.coercedSentinels.keys()].sort()) {
|
|
107
|
+
const perFlag = state.coercedSentinels.get(envId);
|
|
108
|
+
const record = {};
|
|
109
|
+
for (const [flagPath, count] of [...perFlag.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
110
|
+
record[flagPath] = count;
|
|
111
|
+
total += count;
|
|
112
|
+
}
|
|
113
|
+
byEnv[envId] = record;
|
|
114
|
+
}
|
|
115
|
+
return { byEnv, total };
|
|
116
|
+
}
|
|
88
117
|
function getSkippedConfigsImpl() {
|
|
89
118
|
if (state.skippedConfigs.length === 0)
|
|
90
119
|
return null;
|
|
@@ -95,11 +124,13 @@ async function validateAuthImpl(apiKey) {
|
|
|
95
124
|
state.apiKey = apiKey;
|
|
96
125
|
state.droppedOverrides = new Map();
|
|
97
126
|
state.skippedConfigs = [];
|
|
127
|
+
state.coercedSentinels = new Map();
|
|
98
128
|
}
|
|
99
129
|
export const launchSource = {
|
|
100
130
|
fetchChanges(sinceEpochMs) {
|
|
101
131
|
return fetchChangesImpl(sinceEpochMs);
|
|
102
132
|
},
|
|
133
|
+
getCoercedSentinels: getCoercedSentinelsImpl,
|
|
103
134
|
getDroppedOverrides: getDroppedOverridesImpl,
|
|
104
135
|
getSkippedConfigs: getSkippedConfigsImpl,
|
|
105
136
|
listEnvironments: listEnvironmentsImpl,
|
|
@@ -112,4 +143,5 @@ export function __resetLaunchSourceForTests() {
|
|
|
112
143
|
state.envIdMap = null;
|
|
113
144
|
state.droppedOverrides = new Map();
|
|
114
145
|
state.skippedConfigs = [];
|
|
146
|
+
state.coercedSentinels = new Map();
|
|
115
147
|
}
|
package/dist/push/run-push.d.ts
CHANGED
|
@@ -88,6 +88,14 @@ export interface RunPushInput {
|
|
|
88
88
|
message?: string;
|
|
89
89
|
/** `--no-pin-write` — do not offer to write the slug pin into quonfig.json. */
|
|
90
90
|
noPinWrite: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Caller's resolved org slug for this workspace. Used to construct the
|
|
93
|
+
* `<org>/<ws>` pin when the backend's mint-token returns just the bare
|
|
94
|
+
* workspace component (current backend behavior). Optional in tests; if
|
|
95
|
+
* omitted, pin backfill only runs when the backend already returns the
|
|
96
|
+
* slash form.
|
|
97
|
+
*/
|
|
98
|
+
orgSlug?: string;
|
|
91
99
|
/** `--workspace` flag, or the active profile's workspace UUID. Must be non-empty. */
|
|
92
100
|
requestedTarget: string;
|
|
93
101
|
/** `--skip-validate` — suppress the validate step. */
|
package/dist/push/run-push.js
CHANGED
|
@@ -72,7 +72,15 @@ export async function runPush(input, deps) {
|
|
|
72
72
|
// the backend's canonical identity) but we want the pin/origin up front.
|
|
73
73
|
// The pin is `<org>/<ws>`; identity-check still operates on the workspace
|
|
74
74
|
// component until backend responses carry the org slug too (qfg-kr7 epic).
|
|
75
|
-
|
|
75
|
+
// A legacy bare-slug pin is treated as "no pin set" so the post-push
|
|
76
|
+
// backfill can rewrite it to the canonical form.
|
|
77
|
+
let repoPin;
|
|
78
|
+
try {
|
|
79
|
+
repoPin = await readWorkspaceSlug(input.dir);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
repoPin = undefined;
|
|
83
|
+
}
|
|
76
84
|
const repoPinSlug = repoPin?.workspaceSlug;
|
|
77
85
|
const hasGit = await deps.gitOps.isGitRepo(input.dir);
|
|
78
86
|
const remoteOriginUrl = hasGit ? await deps.gitOps.getRemoteOriginUrl(input.dir) : undefined;
|
|
@@ -200,12 +208,13 @@ export async function runPush(input, deps) {
|
|
|
200
208
|
throw new PushFatalError(`Push rejected by server: ${result.message}`, 'BAD_REQUEST');
|
|
201
209
|
}
|
|
202
210
|
if (unpinned && !input.noPinWrite) {
|
|
203
|
-
//
|
|
204
|
-
// it
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
const
|
|
211
|
+
// The backend's mint-token still returns the bare workspace component;
|
|
212
|
+
// combine it with the caller-supplied orgSlug locally. If the backend
|
|
213
|
+
// ever upgrades to slash form, tryParseWorkspacePin handles that case
|
|
214
|
+
// and we use its parts directly. Without either signal, skip — the
|
|
215
|
+
// pin is advisory and a partial slash form would be worse than absent.
|
|
216
|
+
const parsedBackend = tryParseWorkspacePin(backend.workspaceSlug);
|
|
217
|
+
const backendPin = parsedBackend ?? (input.orgSlug ? { orgSlug: input.orgSlug, workspaceSlug: backend.workspaceSlug } : undefined);
|
|
209
218
|
if (backendPin) {
|
|
210
219
|
try {
|
|
211
220
|
await writeWorkspaceSlug(input.dir, backendPin);
|
|
@@ -31,4 +31,7 @@ export interface CloneAndStackPushResult {
|
|
|
31
31
|
export declare class PushConflictError extends Error {
|
|
32
32
|
constructor(message: string);
|
|
33
33
|
}
|
|
34
|
+
export declare class PushHookRejectedError extends Error {
|
|
35
|
+
constructor(message: string);
|
|
36
|
+
}
|
|
34
37
|
export declare const cloneAndStackPush: (opts: CloneAndStackPushOptions) => Promise<CloneAndStackPushResult>;
|
|
@@ -12,6 +12,12 @@ export class PushConflictError extends Error {
|
|
|
12
12
|
this.name = 'PushConflictError';
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
+
export class PushHookRejectedError extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = 'PushHookRejectedError';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
15
21
|
const runGit = async (cwd, args) => {
|
|
16
22
|
try {
|
|
17
23
|
return await runGitSafe(args, { cwd });
|
|
@@ -103,8 +109,15 @@ export const cloneAndStackPush = async (opts) => {
|
|
|
103
109
|
const e = error;
|
|
104
110
|
const stderr = e.stderr ?? '';
|
|
105
111
|
const combined = `${e.message}\n${stderr}`;
|
|
112
|
+
const detail = redactToken(stderr || e.message);
|
|
113
|
+
// Pre-receive / update hook rejections are functionally distinct from fast-forward
|
|
114
|
+
// conflicts: nothing the user can do by re-fetching will help. Route them to a
|
|
115
|
+
// different lead-in so the hook output (e.g. qfg-verify FAILED ...) is the call to action.
|
|
116
|
+
if (/hook declined|\[remote rejected]/i.test(combined)) {
|
|
117
|
+
throw new PushHookRejectedError(`Push to origin/${branch} was rejected by the remote validation hook. Fix the errors below and push again. Original git error:\n${detail}`);
|
|
118
|
+
}
|
|
106
119
|
if (/non-fast-forward|fetch first|rejected/i.test(combined)) {
|
|
107
|
-
throw new PushConflictError(`Push to origin/${branch} was rejected: the remote has changes we do not have locally. Re-run the migrator to pick up those changes, then push again. Original git error:\n${
|
|
120
|
+
throw new PushConflictError(`Push to origin/${branch} was rejected: the remote has changes we do not have locally. Re-run the migrator to pick up those changes, then push again. Original git error:\n${detail}`);
|
|
108
121
|
}
|
|
109
122
|
throw error;
|
|
110
123
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: "0.0.
|
|
1
|
+
declare const _default: "0.0.33";
|
|
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.33';
|
package/oclif.manifest.json
CHANGED
|
@@ -1085,11 +1085,11 @@
|
|
|
1085
1085
|
"args": {},
|
|
1086
1086
|
"description": "Migrate flags and configs from a legacy feature-flag system into a Quonfig workspace.\n\nCurrently supported sources:\n launch — Reforge Launch (ported from launch-migrator)\n\nSee https://docs.quonfig.com/docs/migrating/from-launch for the step-by-step guide,\nand https://docs.quonfig.com/docs/migrating/troubleshooting when something goes sideways.",
|
|
1087
1087
|
"examples": [
|
|
1088
|
-
"<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-
|
|
1088
|
+
"<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-repo",
|
|
1089
1089
|
"<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --workspace acme-prod --push",
|
|
1090
|
-
"<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-
|
|
1091
|
-
"<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-
|
|
1092
|
-
"<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-
|
|
1090
|
+
"<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-repo --reset",
|
|
1091
|
+
"<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-repo --dry-run",
|
|
1092
|
+
"<%= config.bin %> <%= command.id %> --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-repo --staging"
|
|
1093
1093
|
],
|
|
1094
1094
|
"flags": {
|
|
1095
1095
|
"json": {
|
|
@@ -1130,7 +1130,7 @@
|
|
|
1130
1130
|
"type": "option"
|
|
1131
1131
|
},
|
|
1132
1132
|
"dir": {
|
|
1133
|
-
"description": "Target local workspace directory. Defaults to cwd if it looks like a Quonfig workspace, otherwise ./quonfig-
|
|
1133
|
+
"description": "Target local workspace directory. Defaults to cwd if it looks like a Quonfig workspace, otherwise ./quonfig-repo.",
|
|
1134
1134
|
"name": "dir",
|
|
1135
1135
|
"hasDynamicHelp": false,
|
|
1136
1136
|
"multiple": false,
|
|
@@ -2910,5 +2910,5 @@
|
|
|
2910
2910
|
]
|
|
2911
2911
|
}
|
|
2912
2912
|
},
|
|
2913
|
-
"version": "0.0.
|
|
2913
|
+
"version": "0.0.33"
|
|
2914
2914
|
}
|
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.33",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/quonfig/cli/issues"
|
|
7
7
|
},
|