@quonfig/cli 0.0.16 → 0.0.17
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 +33 -33
- package/dist/commands/migrate.js +47 -1
- package/dist/migrate/local-write.d.ts +7 -1
- package/dist/migrate/local-write.js +54 -6
- package/dist/migrate/migration-report.d.ts +18 -0
- package/dist/migrate/migration-report.js +64 -1
- package/dist/migrate/push-to-cloud.d.ts +13 -2
- package/dist/migrate/push-to-cloud.js +76 -5
- package/dist/migrate/source.d.ts +50 -1
- package/dist/migrate/sources/launch/translate.d.ts +27 -1
- package/dist/migrate/sources/launch/translate.js +123 -6
- package/dist/migrate/sources/launch/types.d.ts +3 -0
- package/dist/migrate/sources/launch/zod-to-json-schema.d.ts +9 -0
- package/dist/migrate/sources/launch/zod-to-json-schema.js +521 -0
- package/dist/migrate/sources/launch.js +52 -7
- package/dist/util/get-client.js +5 -4
- package/dist/util/get-valid-token.d.ts +3 -1
- package/dist/util/get-valid-token.js +28 -3
- package/oclif.manifest.json +1 -1
- package/package.json +3 -3
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.17 linux-x64 node-v24.4.1
|
|
18
18
|
$ qfg --help [COMMAND]
|
|
19
19
|
USAGE
|
|
20
20
|
$ qfg COMMAND
|
|
@@ -89,7 +89,7 @@ EXAMPLES
|
|
|
89
89
|
$ qfg config-schema --json-schema # full JSON Schema document (copy into your editor)
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
_See code: [src/commands/config-schema.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
92
|
+
_See code: [src/commands/config-schema.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/config-schema.ts)_
|
|
93
93
|
|
|
94
94
|
## `qfg create NAME`
|
|
95
95
|
|
|
@@ -161,7 +161,7 @@ EXAMPLES
|
|
|
161
161
|
$ qfg set-rollout my.new.flag --environment production --true-percent 20
|
|
162
162
|
```
|
|
163
163
|
|
|
164
|
-
_See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
164
|
+
_See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/create.ts)_
|
|
165
165
|
|
|
166
166
|
## `qfg flag info [NAME]`
|
|
167
167
|
|
|
@@ -361,7 +361,7 @@ EXAMPLES
|
|
|
361
361
|
$ qfg generate --targets node-ts -o ./dist # combine with targets
|
|
362
362
|
```
|
|
363
363
|
|
|
364
|
-
_See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
364
|
+
_See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/generate.ts)_
|
|
365
365
|
|
|
366
366
|
## `qfg generate-new-hex-key`
|
|
367
367
|
|
|
@@ -384,7 +384,7 @@ EXAMPLES
|
|
|
384
384
|
$ qfg generate-new-hex-key
|
|
385
385
|
```
|
|
386
386
|
|
|
387
|
-
_See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
387
|
+
_See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/generate-new-hex-key.ts)_
|
|
388
388
|
|
|
389
389
|
## `qfg get [NAME]`
|
|
390
390
|
|
|
@@ -416,7 +416,7 @@ EXAMPLES
|
|
|
416
416
|
$ qfg get my.config.name --environment=production
|
|
417
417
|
```
|
|
418
418
|
|
|
419
|
-
_See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
419
|
+
_See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/get.ts)_
|
|
420
420
|
|
|
421
421
|
## `qfg info [NAME]`
|
|
422
422
|
|
|
@@ -464,7 +464,7 @@ EXAMPLES
|
|
|
464
464
|
$ qfg info my.config.name --exclude-evaluations # skip 24h stats
|
|
465
465
|
```
|
|
466
466
|
|
|
467
|
-
_See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
467
|
+
_See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/info.ts)_
|
|
468
468
|
|
|
469
469
|
## `qfg init [DIRECTORY]`
|
|
470
470
|
|
|
@@ -504,7 +504,7 @@ EXAMPLES
|
|
|
504
504
|
$ qfg init --dry-run
|
|
505
505
|
```
|
|
506
506
|
|
|
507
|
-
_See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
507
|
+
_See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/init.ts)_
|
|
508
508
|
|
|
509
509
|
## `qfg interactive`
|
|
510
510
|
|
|
@@ -534,7 +534,7 @@ EXAMPLES
|
|
|
534
534
|
$ qfg
|
|
535
535
|
```
|
|
536
536
|
|
|
537
|
-
_See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
537
|
+
_See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/interactive.ts)_
|
|
538
538
|
|
|
539
539
|
## `qfg list`
|
|
540
540
|
|
|
@@ -574,7 +574,7 @@ EXAMPLES
|
|
|
574
574
|
$ qfg list --feature-flags
|
|
575
575
|
```
|
|
576
576
|
|
|
577
|
-
_See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
577
|
+
_See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/list.ts)_
|
|
578
578
|
|
|
579
579
|
## `qfg login`
|
|
580
580
|
|
|
@@ -599,7 +599,7 @@ EXAMPLES
|
|
|
599
599
|
$ qfg login --profile myprofile
|
|
600
600
|
```
|
|
601
601
|
|
|
602
|
-
_See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
602
|
+
_See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/login.ts)_
|
|
603
603
|
|
|
604
604
|
## `qfg logout`
|
|
605
605
|
|
|
@@ -622,7 +622,7 @@ EXAMPLES
|
|
|
622
622
|
$ qfg logout
|
|
623
623
|
```
|
|
624
624
|
|
|
625
|
-
_See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
625
|
+
_See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/logout.ts)_
|
|
626
626
|
|
|
627
627
|
## `qfg mcp`
|
|
628
628
|
|
|
@@ -655,7 +655,7 @@ EXAMPLES
|
|
|
655
655
|
$ qfg mcp --url http://local-app.quonfig-staging.com:3003/api/v1/mcp
|
|
656
656
|
```
|
|
657
657
|
|
|
658
|
-
_See code: [src/commands/mcp.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
658
|
+
_See code: [src/commands/mcp.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/mcp.ts)_
|
|
659
659
|
|
|
660
660
|
## `qfg migrate`
|
|
661
661
|
|
|
@@ -709,7 +709,7 @@ EXAMPLES
|
|
|
709
709
|
$ qfg migrate --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-config --staging
|
|
710
710
|
```
|
|
711
711
|
|
|
712
|
-
_See code: [src/commands/migrate.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
712
|
+
_See code: [src/commands/migrate.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/migrate.ts)_
|
|
713
713
|
|
|
714
714
|
## `qfg migrate doctor`
|
|
715
715
|
|
|
@@ -747,7 +747,7 @@ EXAMPLES
|
|
|
747
747
|
$ qfg migrate doctor --json
|
|
748
748
|
```
|
|
749
749
|
|
|
750
|
-
_See code: [src/commands/migrate/doctor.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
750
|
+
_See code: [src/commands/migrate/doctor.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/migrate/doctor.ts)_
|
|
751
751
|
|
|
752
752
|
## `qfg migrate my-code`
|
|
753
753
|
|
|
@@ -779,7 +779,7 @@ EXAMPLES
|
|
|
779
779
|
$ qfg migrate my-code --dry-run
|
|
780
780
|
```
|
|
781
781
|
|
|
782
|
-
_See code: [src/commands/migrate/my-code.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
782
|
+
_See code: [src/commands/migrate/my-code.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/migrate/my-code.ts)_
|
|
783
783
|
|
|
784
784
|
## `qfg migrate status`
|
|
785
785
|
|
|
@@ -810,7 +810,7 @@ EXAMPLES
|
|
|
810
810
|
$ qfg migrate status --json
|
|
811
811
|
```
|
|
812
812
|
|
|
813
|
-
_See code: [src/commands/migrate/status.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
813
|
+
_See code: [src/commands/migrate/status.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/migrate/status.ts)_
|
|
814
814
|
|
|
815
815
|
## `qfg override [NAME]`
|
|
816
816
|
|
|
@@ -854,7 +854,7 @@ EXAMPLES
|
|
|
854
854
|
$ qfg override my.double.config --value=3.14159
|
|
855
855
|
```
|
|
856
856
|
|
|
857
|
-
_See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
857
|
+
_See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/override.ts)_
|
|
858
858
|
|
|
859
859
|
## `qfg pull`
|
|
860
860
|
|
|
@@ -898,7 +898,7 @@ EXAMPLES
|
|
|
898
898
|
$ qfg pull # uses QUONFIG_DIR env var
|
|
899
899
|
```
|
|
900
900
|
|
|
901
|
-
_See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
901
|
+
_See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/pull.ts)_
|
|
902
902
|
|
|
903
903
|
## `qfg push`
|
|
904
904
|
|
|
@@ -946,7 +946,7 @@ EXAMPLES
|
|
|
946
946
|
$ qfg push --dir ./our-config --yes
|
|
947
947
|
```
|
|
948
948
|
|
|
949
|
-
_See code: [src/commands/push.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
949
|
+
_See code: [src/commands/push.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/push.ts)_
|
|
950
950
|
|
|
951
951
|
## `qfg schema NAME`
|
|
952
952
|
|
|
@@ -984,7 +984,7 @@ EXAMPLES
|
|
|
984
984
|
$ qfg schema my-schema --set-json-schema=@schemas/my-schema.json --protected
|
|
985
985
|
```
|
|
986
986
|
|
|
987
|
-
_See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
987
|
+
_See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/schema.ts)_
|
|
988
988
|
|
|
989
989
|
## `qfg sdk-key`
|
|
990
990
|
|
|
@@ -1011,7 +1011,7 @@ EXAMPLES
|
|
|
1011
1011
|
$ qfg sdk-key revoke <key-id>
|
|
1012
1012
|
```
|
|
1013
1013
|
|
|
1014
|
-
_See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1014
|
+
_See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/sdk-key.ts)_
|
|
1015
1015
|
|
|
1016
1016
|
## `qfg sdk-key create`
|
|
1017
1017
|
|
|
@@ -1043,7 +1043,7 @@ EXAMPLES
|
|
|
1043
1043
|
$ qfg sdk-key create --environment staging --type browser
|
|
1044
1044
|
```
|
|
1045
1045
|
|
|
1046
|
-
_See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1046
|
+
_See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/sdk-key/create.ts)_
|
|
1047
1047
|
|
|
1048
1048
|
## `qfg sdk-key list`
|
|
1049
1049
|
|
|
@@ -1072,7 +1072,7 @@ EXAMPLES
|
|
|
1072
1072
|
$ qfg sdk-key list --environment production
|
|
1073
1073
|
```
|
|
1074
1074
|
|
|
1075
|
-
_See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1075
|
+
_See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/sdk-key/list.ts)_
|
|
1076
1076
|
|
|
1077
1077
|
## `qfg sdk-key revoke KEYID`
|
|
1078
1078
|
|
|
@@ -1099,7 +1099,7 @@ EXAMPLES
|
|
|
1099
1099
|
$ qfg sdk-key revoke a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
|
1100
1100
|
```
|
|
1101
1101
|
|
|
1102
|
-
_See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1102
|
+
_See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/sdk-key/revoke.ts)_
|
|
1103
1103
|
|
|
1104
1104
|
## `qfg set-default [NAME]`
|
|
1105
1105
|
|
|
@@ -1170,7 +1170,7 @@ EXAMPLES
|
|
|
1170
1170
|
$ qfg pull && qfg config-schema
|
|
1171
1171
|
```
|
|
1172
1172
|
|
|
1173
|
-
_See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1173
|
+
_See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/set-default.ts)_
|
|
1174
1174
|
|
|
1175
1175
|
## `qfg set-rollout [NAME]`
|
|
1176
1176
|
|
|
@@ -1247,7 +1247,7 @@ EXAMPLES
|
|
|
1247
1247
|
$ qfg set-rollout my.variant.flag --environment production --weights "a:33,b:33,c:34" --hash-by user.id
|
|
1248
1248
|
```
|
|
1249
1249
|
|
|
1250
|
-
_See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1250
|
+
_See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/set-rollout.ts)_
|
|
1251
1251
|
|
|
1252
1252
|
## `qfg sync`
|
|
1253
1253
|
|
|
@@ -1279,7 +1279,7 @@ EXAMPLES
|
|
|
1279
1279
|
$ qfg sync --watch --dir ./our-config --interval 30
|
|
1280
1280
|
```
|
|
1281
1281
|
|
|
1282
|
-
_See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1282
|
+
_See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/sync.ts)_
|
|
1283
1283
|
|
|
1284
1284
|
## `qfg toggle [NAME]`
|
|
1285
1285
|
|
|
@@ -1381,7 +1381,7 @@ EXAMPLES
|
|
|
1381
1381
|
$ qfg verify --json
|
|
1382
1382
|
```
|
|
1383
1383
|
|
|
1384
|
-
_See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1384
|
+
_See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/verify.ts)_
|
|
1385
1385
|
|
|
1386
1386
|
## `qfg whoami`
|
|
1387
1387
|
|
|
@@ -1404,7 +1404,7 @@ EXAMPLES
|
|
|
1404
1404
|
$ qfg whoami
|
|
1405
1405
|
```
|
|
1406
1406
|
|
|
1407
|
-
_See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1407
|
+
_See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/whoami.ts)_
|
|
1408
1408
|
|
|
1409
1409
|
## `qfg workspace`
|
|
1410
1410
|
|
|
@@ -1427,7 +1427,7 @@ EXAMPLES
|
|
|
1427
1427
|
$ qfg workspace
|
|
1428
1428
|
```
|
|
1429
1429
|
|
|
1430
|
-
_See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1430
|
+
_See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/workspace.ts)_
|
|
1431
1431
|
|
|
1432
1432
|
## `qfg workspace bootstrap`
|
|
1433
1433
|
|
|
@@ -1460,7 +1460,7 @@ EXAMPLES
|
|
|
1460
1460
|
$ qfg workspace bootstrap --dir ./our-config --skip-validate
|
|
1461
1461
|
```
|
|
1462
1462
|
|
|
1463
|
-
_See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1463
|
+
_See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/workspace/bootstrap.ts)_
|
|
1464
1464
|
|
|
1465
1465
|
## `qfg workspace switch [SLUG]`
|
|
1466
1466
|
|
|
@@ -1488,7 +1488,7 @@ EXAMPLES
|
|
|
1488
1488
|
$ qfg workspace switch my-workspace-slug
|
|
1489
1489
|
```
|
|
1490
1490
|
|
|
1491
|
-
_See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1491
|
+
_See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.0.17/src/commands/workspace/switch.ts)_
|
|
1492
1492
|
<!-- commandsstop -->
|
|
1493
1493
|
|
|
1494
1494
|
## Local Development
|
package/dist/commands/migrate.js
CHANGED
|
@@ -7,7 +7,7 @@ import { applyLocalMigration } from '../migrate/local-write.js';
|
|
|
7
7
|
import { PushConflictError } from '../util/clone-and-stack-push.js';
|
|
8
8
|
import { pushMigrationToCloud } from '../migrate/push-to-cloud.js';
|
|
9
9
|
import { UnknownSourceError, getSource } from '../migrate/registry.js';
|
|
10
|
-
import { NotYetImplementedError } from '../migrate/source.js';
|
|
10
|
+
import { NotYetImplementedError, } from '../migrate/source.js';
|
|
11
11
|
import { applyLaunchBaseUrl } from '../migrate/sources/launch/api.js';
|
|
12
12
|
import { mintGiteaToken } from '../util/gitea-api.js';
|
|
13
13
|
import { displayUrl } from '../util/git-ops.js';
|
|
@@ -104,6 +104,7 @@ export default class Migrate extends BaseCommand {
|
|
|
104
104
|
return this.err(error.message);
|
|
105
105
|
throw error;
|
|
106
106
|
}
|
|
107
|
+
let duplicateResolutionsForWarn = null;
|
|
107
108
|
try {
|
|
108
109
|
await source.validateAuth(flags['api-key']);
|
|
109
110
|
const environments = await source.listEnvironments();
|
|
@@ -148,12 +149,14 @@ export default class Migrate extends BaseCommand {
|
|
|
148
149
|
const result = await pushMigrationToCloud({
|
|
149
150
|
changes: toProcess,
|
|
150
151
|
commitMessage: buildCommitMessage(flags.from, toProcess.length),
|
|
152
|
+
environments,
|
|
151
153
|
importState,
|
|
152
154
|
localDir: dir,
|
|
153
155
|
remoteUrl: repoUrl,
|
|
154
156
|
reportData: buildReportData(flags.from, toProcess.length),
|
|
155
157
|
source,
|
|
156
158
|
});
|
|
159
|
+
duplicateResolutionsForWarn = result.duplicateResolutions;
|
|
157
160
|
this.log(result.committed
|
|
158
161
|
? `Pushed ${toProcess.length} change(s). commit=${result.commitSha?.slice(0, 8) ?? ''} action=${result.action}`
|
|
159
162
|
: `No net changes produced by this run. Nothing to commit or push.`);
|
|
@@ -188,6 +191,7 @@ export default class Migrate extends BaseCommand {
|
|
|
188
191
|
reportData: buildReportData(flags.from, toProcess.length),
|
|
189
192
|
source,
|
|
190
193
|
});
|
|
194
|
+
duplicateResolutionsForWarn = localResult.duplicateResolutions;
|
|
191
195
|
this.log(localResult.committed
|
|
192
196
|
? `Committed ${toProcess.length} change(s). commit=${localResult.commitSha?.slice(0, 8) ?? ''} action=${localResult.action}`
|
|
193
197
|
: `No net changes produced by this run. Nothing to commit.`);
|
|
@@ -203,6 +207,13 @@ export default class Migrate extends BaseCommand {
|
|
|
203
207
|
return this.err(error.message);
|
|
204
208
|
throw error;
|
|
205
209
|
}
|
|
210
|
+
finally {
|
|
211
|
+
// Surface source-level warnings even if apply/push threw (e.g.
|
|
212
|
+
// detectDuplicateKeys). The source accumulator holds the data regardless.
|
|
213
|
+
warnAboutDroppedOverrides(this, source.getDroppedOverrides?.() ?? null);
|
|
214
|
+
warnAboutSkippedConfigs(this, source.getSkippedConfigs?.() ?? null);
|
|
215
|
+
warnAboutDuplicateResolutions(this, duplicateResolutionsForWarn);
|
|
216
|
+
}
|
|
206
217
|
}
|
|
207
218
|
async resolvePushWorkspace(slugOrId) {
|
|
208
219
|
const authConfig = await loadAuthConfig();
|
|
@@ -250,6 +261,41 @@ function buildReportData(source, count) {
|
|
|
250
261
|
unsupportedFeatures: [],
|
|
251
262
|
};
|
|
252
263
|
}
|
|
264
|
+
function warnAboutDroppedOverrides(cmd, dropped) {
|
|
265
|
+
if (!dropped || dropped.total === 0)
|
|
266
|
+
return;
|
|
267
|
+
const envIds = Object.keys(dropped.byEnv).sort();
|
|
268
|
+
cmd.warn(`Dropped ${dropped.total} override section(s) referencing ${envIds.length} env ID(s) not present in the source's env list (likely archived/deleted):`);
|
|
269
|
+
for (const envId of envIds) {
|
|
270
|
+
const perFlag = dropped.byEnv[envId];
|
|
271
|
+
const totalForEnv = Object.values(perFlag).reduce((s, n) => s + n, 0);
|
|
272
|
+
cmd.warn(` env-${envId}: ${totalForEnv} dropped from ${Object.keys(perFlag).length} flag(s)`);
|
|
273
|
+
for (const flagPath of Object.keys(perFlag).sort()) {
|
|
274
|
+
cmd.warn(` - ${flagPath} (${perFlag[flagPath]})`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
cmd.warn(`If any of these envs are still in use, restore them in the source system and re-run the migration. Full detail is also written to MIGRATION_REPORT.md.`);
|
|
278
|
+
}
|
|
279
|
+
function warnAboutDuplicateResolutions(cmd, resolved) {
|
|
280
|
+
if (!resolved || resolved.total === 0)
|
|
281
|
+
return;
|
|
282
|
+
cmd.warn(`Resolved ${resolved.total} cross-type key collision(s) in the source data by keeping the config side and deleting the other type(s). Review each and clean up the source system so the collision stops recurring:`);
|
|
283
|
+
const sorted = [...resolved.entries].sort((a, b) => a.key.localeCompare(b.key));
|
|
284
|
+
for (const entry of sorted) {
|
|
285
|
+
cmd.warn(` ${entry.key} (${entry.collisionTypes.join(', ')}): kept ${entry.kept}, deleted ${entry.deleted.join(', ')}`);
|
|
286
|
+
}
|
|
287
|
+
cmd.warn('Full detail is also written to MIGRATION_REPORT.md.');
|
|
288
|
+
}
|
|
289
|
+
function warnAboutSkippedConfigs(cmd, skipped) {
|
|
290
|
+
if (!skipped || skipped.total === 0)
|
|
291
|
+
return;
|
|
292
|
+
cmd.warn(`Skipped ${skipped.total} invalid config(s) from the source system — these were not written to the workspace. Fix each in the source system and re-run:`);
|
|
293
|
+
const sorted = [...skipped.entries].sort((a, b) => a.key.localeCompare(b.key));
|
|
294
|
+
for (const entry of sorted) {
|
|
295
|
+
cmd.warn(` ${entry.key}: ${entry.reason}`);
|
|
296
|
+
}
|
|
297
|
+
cmd.warn('Full detail is also written to MIGRATION_REPORT.md.');
|
|
298
|
+
}
|
|
253
299
|
function resolveTargetDir(dirFlag, cwd) {
|
|
254
300
|
if (dirFlag)
|
|
255
301
|
return path.resolve(cwd, dirFlag);
|
|
@@ -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 { LegacyChange, MigrationSource } from './source.js';
|
|
4
|
+
import type { 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;
|
|
@@ -22,5 +22,11 @@ export interface ApplyLocalMigrationResult {
|
|
|
22
22
|
commitSha: string | null;
|
|
23
23
|
/** `false` if translate produced no net changes — nothing was committed. */
|
|
24
24
|
committed: boolean;
|
|
25
|
+
/** Override sections dropped during translate because env.id was unknown. Null if none. */
|
|
26
|
+
droppedOverrides: DroppedOverrideSummary | null;
|
|
27
|
+
/** Cross-type key collisions resolved by preferring the config side. Null if none. */
|
|
28
|
+
duplicateResolutions: DuplicateResolutionSummary | null;
|
|
29
|
+
/** Configs soft-skipped during translate due to invalid source data. Null if none. */
|
|
30
|
+
skippedConfigs: SkippedConfigSummary | null;
|
|
25
31
|
}
|
|
26
32
|
export declare const applyLocalMigration: (opts: ApplyLocalMigrationOptions) => Promise<ApplyLocalMigrationResult>;
|
|
@@ -4,6 +4,7 @@ import * as path from 'node:path';
|
|
|
4
4
|
import * as util from 'node:util';
|
|
5
5
|
import { removeQfFromGitignore, writeImportState } from './import-state.js';
|
|
6
6
|
import { writeMigrationReport } from './migration-report.js';
|
|
7
|
+
import { detectDuplicateKeys } from './sources/launch/translate.js';
|
|
7
8
|
import { MIGRATOR_IDENTITY } from '../util/clone-and-stack-push.js';
|
|
8
9
|
const execFile = util.promisify(execFileCb);
|
|
9
10
|
const isGitRepo = async (dir) => {
|
|
@@ -49,14 +50,45 @@ const ensureLocalRepo = async (localDir, branch) => {
|
|
|
49
50
|
return 'initialized';
|
|
50
51
|
};
|
|
51
52
|
const writeQuonfigFiles = (dir, changes, source) => {
|
|
53
|
+
const livePaths = new Map();
|
|
52
54
|
for (const change of changes) {
|
|
53
55
|
const files = source.translate(change);
|
|
54
56
|
for (const file of files) {
|
|
55
57
|
const full = path.join(dir, file.path);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
if (file.deleted) {
|
|
59
|
+
try {
|
|
60
|
+
fs.unlinkSync(full);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error.code !== 'ENOENT')
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
livePaths.delete(file.path);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
if (file.contents === undefined) {
|
|
70
|
+
throw new Error(`translate() emitted a write op for ${file.path} without contents`);
|
|
71
|
+
}
|
|
72
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
73
|
+
fs.writeFileSync(full, file.contents);
|
|
74
|
+
livePaths.set(file.path, true);
|
|
75
|
+
}
|
|
58
76
|
}
|
|
59
77
|
}
|
|
78
|
+
const resolutions = detectDuplicateKeys([...livePaths.keys()].map((p) => ({ path: p })));
|
|
79
|
+
for (const resolution of resolutions) {
|
|
80
|
+
for (const toDelete of resolution.deleted) {
|
|
81
|
+
try {
|
|
82
|
+
fs.unlinkSync(path.join(dir, toDelete));
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error.code !== 'ENOENT')
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
livePaths.delete(toDelete);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return resolutions;
|
|
60
92
|
};
|
|
61
93
|
export const applyLocalMigration = async (opts) => {
|
|
62
94
|
const branch = opts.branch ?? 'main';
|
|
@@ -65,13 +97,22 @@ export const applyLocalMigration = async (opts) => {
|
|
|
65
97
|
await execFile('git', ['-C', opts.localDir, 'config', 'user.name', author.name]);
|
|
66
98
|
await execFile('git', ['-C', opts.localDir, 'config', 'user.email', author.email]);
|
|
67
99
|
ensureQuonfigJson(opts.localDir, opts.environments);
|
|
68
|
-
writeQuonfigFiles(opts.localDir, opts.changes, opts.source);
|
|
100
|
+
const resolutionEntries = writeQuonfigFiles(opts.localDir, opts.changes, opts.source);
|
|
69
101
|
removeQfFromGitignore(opts.localDir);
|
|
70
102
|
writeImportState(opts.localDir, opts.importState);
|
|
71
|
-
|
|
103
|
+
const droppedOverrides = opts.source.getDroppedOverrides?.() ?? null;
|
|
104
|
+
const skippedConfigs = opts.source.getSkippedConfigs?.() ?? null;
|
|
105
|
+
const duplicateResolutions = resolutionEntries.length > 0 ? { entries: resolutionEntries, total: resolutionEntries.length } : null;
|
|
106
|
+
const reportData = {
|
|
107
|
+
...opts.reportData,
|
|
108
|
+
...(droppedOverrides ? { droppedOverrides } : {}),
|
|
109
|
+
...(duplicateResolutions ? { duplicateResolutions } : {}),
|
|
110
|
+
...(skippedConfigs ? { skippedConfigs } : {}),
|
|
111
|
+
};
|
|
112
|
+
writeMigrationReport(opts.localDir, reportData);
|
|
72
113
|
await execFile('git', ['-C', opts.localDir, 'add', '--all']);
|
|
73
114
|
if (!(await hasStagedChanges(opts.localDir))) {
|
|
74
|
-
return { action, committed: false, commitSha: null };
|
|
115
|
+
return { action, committed: false, commitSha: null, droppedOverrides, duplicateResolutions, skippedConfigs };
|
|
75
116
|
}
|
|
76
117
|
await gitCommitFromStdin(opts.localDir, opts.commitMessage, {
|
|
77
118
|
...process.env,
|
|
@@ -81,5 +122,12 @@ export const applyLocalMigration = async (opts) => {
|
|
|
81
122
|
GIT_COMMITTER_NAME: author.name,
|
|
82
123
|
});
|
|
83
124
|
const { stdout: sha } = await execFile('git', ['-C', opts.localDir, 'rev-parse', 'HEAD']);
|
|
84
|
-
return {
|
|
125
|
+
return {
|
|
126
|
+
action,
|
|
127
|
+
commitSha: sha.trim(),
|
|
128
|
+
committed: true,
|
|
129
|
+
droppedOverrides,
|
|
130
|
+
duplicateResolutions,
|
|
131
|
+
skippedConfigs,
|
|
132
|
+
};
|
|
85
133
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IdentifierMap } from './identifier-map.js';
|
|
2
|
+
import type { DroppedOverrideSummary, DuplicateResolutionSummary, SkippedConfigSummary } from './source.js';
|
|
2
3
|
export interface MigrationReportCounts {
|
|
3
4
|
environmentsMapped: number;
|
|
4
5
|
flagsMigrated: number;
|
|
@@ -29,11 +30,28 @@ export interface FollowUpChecklist {
|
|
|
29
30
|
export interface MigrationReportData {
|
|
30
31
|
cleanMappings: CleanMapping[];
|
|
31
32
|
counts: MigrationReportCounts;
|
|
33
|
+
/**
|
|
34
|
+
* Override sections that were dropped during translate() because the env.id was not
|
|
35
|
+
* present in the source's env map (e.g. archived/deleted Reforge envs). Null when
|
|
36
|
+
* nothing was dropped.
|
|
37
|
+
*/
|
|
38
|
+
droppedOverrides?: DroppedOverrideSummary | null;
|
|
32
39
|
dryRun: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Cross-type key collisions resolved by keeping the config side and deleting
|
|
42
|
+
* the non-config type(s). Null when none were detected. Customers should
|
|
43
|
+
* review each entry and clean up the source data to avoid the collision.
|
|
44
|
+
*/
|
|
45
|
+
duplicateResolutions?: DuplicateResolutionSummary | null;
|
|
33
46
|
environmentMap: EnvironmentMapEntry[];
|
|
34
47
|
followUp: FollowUpChecklist;
|
|
35
48
|
identifierMap: IdentifierMap;
|
|
36
49
|
lossyMappings: LossyMapping[];
|
|
50
|
+
/**
|
|
51
|
+
* Configs soft-skipped by translate() because the source data was invalid
|
|
52
|
+
* (e.g. variant/valueType mismatch). Null when nothing was skipped.
|
|
53
|
+
*/
|
|
54
|
+
skippedConfigs?: SkippedConfigSummary | null;
|
|
37
55
|
source: string;
|
|
38
56
|
unsupportedFeatures: UnsupportedFeature[];
|
|
39
57
|
}
|
|
@@ -49,6 +49,59 @@ const renderIdentifierMap = (map) => {
|
|
|
49
49
|
const rows = sorted.map((k) => `| \`${k}\` | \`${map[k]}\` |`);
|
|
50
50
|
return [header, '', '| Legacy key | Quonfig key |', '| --- | --- |', ...rows].join('\n');
|
|
51
51
|
};
|
|
52
|
+
const renderDroppedOverrides = (dropped) => {
|
|
53
|
+
if (!dropped || dropped.total === 0)
|
|
54
|
+
return null;
|
|
55
|
+
const envIds = Object.keys(dropped.byEnv).sort();
|
|
56
|
+
const lines = [
|
|
57
|
+
'## Dropped override sections',
|
|
58
|
+
'',
|
|
59
|
+
`Dropped **${dropped.total}** override section(s) referencing **${envIds.length}** env ID(s) not present in the source's env list (likely archived/deleted). If any of these envs are still in use, restore them in the source system and re-run the migration.`,
|
|
60
|
+
];
|
|
61
|
+
for (const envId of envIds) {
|
|
62
|
+
const perFlag = dropped.byEnv[envId];
|
|
63
|
+
const totalForEnv = Object.values(perFlag).reduce((s, n) => s + n, 0);
|
|
64
|
+
const flagCount = Object.keys(perFlag).length;
|
|
65
|
+
lines.push('', `### env-${envId} — ${totalForEnv} dropped from ${flagCount} flag(s)`, '');
|
|
66
|
+
const sorted = Object.keys(perFlag).sort();
|
|
67
|
+
for (const flagPath of sorted) {
|
|
68
|
+
lines.push(`- \`${flagPath}\` (${perFlag[flagPath]})`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return lines.join('\n');
|
|
72
|
+
};
|
|
73
|
+
const renderDuplicateResolutions = (resolved) => {
|
|
74
|
+
if (!resolved || resolved.total === 0)
|
|
75
|
+
return null;
|
|
76
|
+
const lines = [
|
|
77
|
+
'## Resolved cross-type duplicates',
|
|
78
|
+
'',
|
|
79
|
+
`Reforge had **${resolved.total}** key(s) present as both a config and a feature_flag (or other types) at the same time. qfg requires globally-unique keys, so the migrator kept the **config** side and deleted the other type(s). Review each and clean up the source system so the collision stops recurring.`,
|
|
80
|
+
'',
|
|
81
|
+
];
|
|
82
|
+
const sorted = [...resolved.entries].sort((a, b) => a.key.localeCompare(b.key));
|
|
83
|
+
for (const entry of sorted) {
|
|
84
|
+
lines.push(`- \`${entry.key}\` (${entry.collisionTypes.join(', ')}): kept \`${entry.kept}\`, deleted ${entry.deleted
|
|
85
|
+
.map((p) => `\`${p}\``)
|
|
86
|
+
.join(', ')}`);
|
|
87
|
+
}
|
|
88
|
+
return lines.join('\n');
|
|
89
|
+
};
|
|
90
|
+
const renderSkippedConfigs = (skipped) => {
|
|
91
|
+
if (!skipped || skipped.total === 0)
|
|
92
|
+
return null;
|
|
93
|
+
const lines = [
|
|
94
|
+
'## Skipped invalid configs',
|
|
95
|
+
'',
|
|
96
|
+
`Skipped **${skipped.total}** config(s) with structurally invalid source data — the migrator refused to emit them rather than ship broken data. Fix each in the source system and re-run.`,
|
|
97
|
+
'',
|
|
98
|
+
];
|
|
99
|
+
const sorted = [...skipped.entries].sort((a, b) => a.key.localeCompare(b.key));
|
|
100
|
+
for (const entry of sorted) {
|
|
101
|
+
lines.push(`- \`${entry.key}\` — ${entry.reason}`);
|
|
102
|
+
}
|
|
103
|
+
return lines.join('\n');
|
|
104
|
+
};
|
|
52
105
|
const renderFollowUp = (followUp) => {
|
|
53
106
|
const must = followUp.mustFixBeforeCutover.length === 0
|
|
54
107
|
? NONE
|
|
@@ -73,7 +126,17 @@ export const buildMigrationReport = (data) => {
|
|
|
73
126
|
if (data.dryRun) {
|
|
74
127
|
sections.push('> **DRY RUN** — no files were written to the workspace during this run.');
|
|
75
128
|
}
|
|
76
|
-
sections.push(`# Migration Report (from ${data.source})`, `_Reflects only the changes produced by this run. Re-running overwrites this file._`, renderCounts(data.counts), renderCleanMappings(data.cleanMappings), renderLossyMappings(data.lossyMappings), renderUnsupported(data.unsupportedFeatures), renderEnvironmentMap(data.environmentMap), renderIdentifierMap(data.identifierMap)
|
|
129
|
+
sections.push(`# Migration Report (from ${data.source})`, `_Reflects only the changes produced by this run. Re-running overwrites this file._`, renderCounts(data.counts), renderCleanMappings(data.cleanMappings), renderLossyMappings(data.lossyMappings), renderUnsupported(data.unsupportedFeatures), renderEnvironmentMap(data.environmentMap), renderIdentifierMap(data.identifierMap));
|
|
130
|
+
const dropped = renderDroppedOverrides(data.droppedOverrides);
|
|
131
|
+
if (dropped !== null)
|
|
132
|
+
sections.push(dropped);
|
|
133
|
+
const resolved = renderDuplicateResolutions(data.duplicateResolutions);
|
|
134
|
+
if (resolved !== null)
|
|
135
|
+
sections.push(resolved);
|
|
136
|
+
const skipped = renderSkippedConfigs(data.skippedConfigs);
|
|
137
|
+
if (skipped !== null)
|
|
138
|
+
sections.push(skipped);
|
|
139
|
+
sections.push(renderFollowUp(data.followUp));
|
|
77
140
|
return sections.join('\n\n') + '\n';
|
|
78
141
|
};
|
|
79
142
|
export const migrationReportPath = (outputDir) => path.join(outputDir, 'MIGRATION_REPORT.md');
|
|
@@ -1,15 +1,26 @@
|
|
|
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 type { LegacyChange, MigrationSource } from './source.js';
|
|
4
|
+
import type { DroppedOverrideSummary, DuplicateResolutionSummary, LegacyChange, MigrationSource, SkippedConfigSummary } from './source.js';
|
|
5
5
|
export interface PushMigrationToCloudOptions {
|
|
6
6
|
branch?: string;
|
|
7
7
|
changes: LegacyChange[];
|
|
8
8
|
commitMessage: string;
|
|
9
|
+
/**
|
|
10
|
+
* Source-side environments (slugified). Additively merged into the target
|
|
11
|
+
* workspace's quonfig.json so flag files that reference these envs verify
|
|
12
|
+
* cleanly. Existing target-only envs are preserved.
|
|
13
|
+
*/
|
|
14
|
+
environments?: string[];
|
|
9
15
|
importState: ImportState;
|
|
10
16
|
localDir: string;
|
|
11
17
|
remoteUrl: string;
|
|
12
18
|
reportData: MigrationReportData;
|
|
13
19
|
source: MigrationSource;
|
|
14
20
|
}
|
|
15
|
-
export
|
|
21
|
+
export interface PushMigrationToCloudResult extends CloneAndStackPushResult {
|
|
22
|
+
droppedOverrides: DroppedOverrideSummary | null;
|
|
23
|
+
duplicateResolutions: DuplicateResolutionSummary | null;
|
|
24
|
+
skippedConfigs: SkippedConfigSummary | null;
|
|
25
|
+
}
|
|
26
|
+
export declare const pushMigrationToCloud: (opts: PushMigrationToCloudOptions) => Promise<PushMigrationToCloudResult>;
|