@quonfig/cli 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -50
- package/dist/verify/map-key-order.d.ts +52 -0
- package/dist/verify/map-key-order.js +246 -0
- package/dist/verify/schema-validator.js +3 -3
- package/dist/verify/standalone.js +54 -22
- package/dist/verify/validate.d.ts +9 -6
- package/dist/verify/validate.js +160 -6
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ $ npm install -g @quonfig/cli
|
|
|
17
17
|
$ qfg COMMAND
|
|
18
18
|
running command...
|
|
19
19
|
$ qfg (--version)
|
|
20
|
-
@quonfig/cli/0.2.
|
|
20
|
+
@quonfig/cli/0.2.2 linux-x64 node-v24.4.1
|
|
21
21
|
$ qfg --help [COMMAND]
|
|
22
22
|
USAGE
|
|
23
23
|
$ qfg COMMAND
|
|
@@ -201,7 +201,7 @@ EXAMPLES
|
|
|
201
201
|
$ qfg activity restore my.flag --yes
|
|
202
202
|
```
|
|
203
203
|
|
|
204
|
-
_See code: [src/commands/activity.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
204
|
+
_See code: [src/commands/activity.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/activity.ts)_
|
|
205
205
|
|
|
206
206
|
## `qfg activity deleted`
|
|
207
207
|
|
|
@@ -230,7 +230,7 @@ EXAMPLES
|
|
|
230
230
|
$ qfg activity deleted --json
|
|
231
231
|
```
|
|
232
232
|
|
|
233
|
-
_See code: [src/commands/activity/deleted.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
233
|
+
_See code: [src/commands/activity/deleted.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/activity/deleted.ts)_
|
|
234
234
|
|
|
235
235
|
## `qfg activity feed`
|
|
236
236
|
|
|
@@ -265,7 +265,7 @@ EXAMPLES
|
|
|
265
265
|
$ qfg activity feed --json
|
|
266
266
|
```
|
|
267
267
|
|
|
268
|
-
_See code: [src/commands/activity/feed.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
268
|
+
_See code: [src/commands/activity/feed.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/activity/feed.ts)_
|
|
269
269
|
|
|
270
270
|
## `qfg activity history [NAME]`
|
|
271
271
|
|
|
@@ -299,7 +299,7 @@ EXAMPLES
|
|
|
299
299
|
$ qfg activity history request.timeout --json
|
|
300
300
|
```
|
|
301
301
|
|
|
302
|
-
_See code: [src/commands/activity/history.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
302
|
+
_See code: [src/commands/activity/history.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/activity/history.ts)_
|
|
303
303
|
|
|
304
304
|
## `qfg activity restore [NAME]`
|
|
305
305
|
|
|
@@ -338,7 +338,7 @@ EXAMPLES
|
|
|
338
338
|
$ qfg activity restore request.timeout --type config --yes
|
|
339
339
|
```
|
|
340
340
|
|
|
341
|
-
_See code: [src/commands/activity/restore.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
341
|
+
_See code: [src/commands/activity/restore.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/activity/restore.ts)_
|
|
342
342
|
|
|
343
343
|
## `qfg audit-log [NAME]`
|
|
344
344
|
|
|
@@ -366,7 +366,7 @@ EXAMPLES
|
|
|
366
366
|
$ qfg audit-log my.flag
|
|
367
367
|
```
|
|
368
368
|
|
|
369
|
-
_See code: [src/commands/audit-log.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
369
|
+
_See code: [src/commands/audit-log.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/audit-log.ts)_
|
|
370
370
|
|
|
371
371
|
## `qfg cleanup`
|
|
372
372
|
|
|
@@ -407,7 +407,7 @@ EXAMPLES
|
|
|
407
407
|
$ qfg cleanup status my.flag.key
|
|
408
408
|
```
|
|
409
409
|
|
|
410
|
-
_See code: [src/commands/cleanup.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
410
|
+
_See code: [src/commands/cleanup.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/cleanup.ts)_
|
|
411
411
|
|
|
412
412
|
## `qfg cleanup list`
|
|
413
413
|
|
|
@@ -453,7 +453,7 @@ EXAMPLES
|
|
|
453
453
|
$ qfg cleanup list --json
|
|
454
454
|
```
|
|
455
455
|
|
|
456
|
-
_See code: [src/commands/cleanup/list.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
456
|
+
_See code: [src/commands/cleanup/list.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/cleanup/list.ts)_
|
|
457
457
|
|
|
458
458
|
## `qfg cleanup remove [NAME]`
|
|
459
459
|
|
|
@@ -498,7 +498,7 @@ EXAMPLES
|
|
|
498
498
|
$ qfg cleanup remove my.flag.key --json
|
|
499
499
|
```
|
|
500
500
|
|
|
501
|
-
_See code: [src/commands/cleanup/remove.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
501
|
+
_See code: [src/commands/cleanup/remove.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/cleanup/remove.ts)_
|
|
502
502
|
|
|
503
503
|
## `qfg cleanup status [NAME]`
|
|
504
504
|
|
|
@@ -538,7 +538,7 @@ EXAMPLES
|
|
|
538
538
|
$ qfg cleanup status my.flag.key --json
|
|
539
539
|
```
|
|
540
540
|
|
|
541
|
-
_See code: [src/commands/cleanup/status.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
541
|
+
_See code: [src/commands/cleanup/status.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/cleanup/status.ts)_
|
|
542
542
|
|
|
543
543
|
## `qfg cleanup verify [NAME]`
|
|
544
544
|
|
|
@@ -584,7 +584,7 @@ EXAMPLES
|
|
|
584
584
|
$ qfg cleanup verify my.flag.key && qfg delete my.flag.key
|
|
585
585
|
```
|
|
586
586
|
|
|
587
|
-
_See code: [src/commands/cleanup/verify.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
587
|
+
_See code: [src/commands/cleanup/verify.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/cleanup/verify.ts)_
|
|
588
588
|
|
|
589
589
|
## `qfg config-schema`
|
|
590
590
|
|
|
@@ -613,7 +613,7 @@ EXAMPLES
|
|
|
613
613
|
$ qfg config-schema --json-schema # full JSON Schema document (copy into your editor)
|
|
614
614
|
```
|
|
615
615
|
|
|
616
|
-
_See code: [src/commands/config-schema.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
616
|
+
_See code: [src/commands/config-schema.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/config-schema.ts)_
|
|
617
617
|
|
|
618
618
|
## `qfg create NAME`
|
|
619
619
|
|
|
@@ -700,7 +700,7 @@ EXAMPLES
|
|
|
700
700
|
$ qfg set-rollout my.new.flag --environment production --true-percent 20
|
|
701
701
|
```
|
|
702
702
|
|
|
703
|
-
_See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
703
|
+
_See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/create.ts)_
|
|
704
704
|
|
|
705
705
|
## `qfg delete NAME`
|
|
706
706
|
|
|
@@ -746,7 +746,7 @@ EXAMPLES
|
|
|
746
746
|
$ qfg delete my.config --yes
|
|
747
747
|
```
|
|
748
748
|
|
|
749
|
-
_See code: [src/commands/delete.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
749
|
+
_See code: [src/commands/delete.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/delete.ts)_
|
|
750
750
|
|
|
751
751
|
## `qfg flag info [NAME]`
|
|
752
752
|
|
|
@@ -950,7 +950,7 @@ EXAMPLES
|
|
|
950
950
|
$ qfg generate --targets node-ts -o ./dist # combine with targets
|
|
951
951
|
```
|
|
952
952
|
|
|
953
|
-
_See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
953
|
+
_See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/generate.ts)_
|
|
954
954
|
|
|
955
955
|
## `qfg generate-new-hex-key`
|
|
956
956
|
|
|
@@ -973,7 +973,7 @@ EXAMPLES
|
|
|
973
973
|
$ qfg generate-new-hex-key
|
|
974
974
|
```
|
|
975
975
|
|
|
976
|
-
_See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
976
|
+
_See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/generate-new-hex-key.ts)_
|
|
977
977
|
|
|
978
978
|
## `qfg get [NAME]`
|
|
979
979
|
|
|
@@ -1005,7 +1005,7 @@ EXAMPLES
|
|
|
1005
1005
|
$ qfg get my.config.name --environment=production
|
|
1006
1006
|
```
|
|
1007
1007
|
|
|
1008
|
-
_See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1008
|
+
_See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/get.ts)_
|
|
1009
1009
|
|
|
1010
1010
|
## `qfg history NAME`
|
|
1011
1011
|
|
|
@@ -1031,7 +1031,7 @@ EXAMPLES
|
|
|
1031
1031
|
$ qfg history my.flag
|
|
1032
1032
|
```
|
|
1033
1033
|
|
|
1034
|
-
_See code: [src/commands/history.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1034
|
+
_See code: [src/commands/history.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/history.ts)_
|
|
1035
1035
|
|
|
1036
1036
|
## `qfg info [NAME]`
|
|
1037
1037
|
|
|
@@ -1079,7 +1079,7 @@ EXAMPLES
|
|
|
1079
1079
|
$ qfg info my.config.name --exclude-evaluations # skip 24h stats
|
|
1080
1080
|
```
|
|
1081
1081
|
|
|
1082
|
-
_See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1082
|
+
_See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/info.ts)_
|
|
1083
1083
|
|
|
1084
1084
|
## `qfg init [DIRECTORY]`
|
|
1085
1085
|
|
|
@@ -1119,7 +1119,7 @@ EXAMPLES
|
|
|
1119
1119
|
$ qfg init --dry-run
|
|
1120
1120
|
```
|
|
1121
1121
|
|
|
1122
|
-
_See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1122
|
+
_See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/init.ts)_
|
|
1123
1123
|
|
|
1124
1124
|
## `qfg interactive`
|
|
1125
1125
|
|
|
@@ -1149,7 +1149,7 @@ EXAMPLES
|
|
|
1149
1149
|
$ qfg
|
|
1150
1150
|
```
|
|
1151
1151
|
|
|
1152
|
-
_See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1152
|
+
_See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/interactive.ts)_
|
|
1153
1153
|
|
|
1154
1154
|
## `qfg list`
|
|
1155
1155
|
|
|
@@ -1189,7 +1189,7 @@ EXAMPLES
|
|
|
1189
1189
|
$ qfg list --feature-flags
|
|
1190
1190
|
```
|
|
1191
1191
|
|
|
1192
|
-
_See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1192
|
+
_See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/list.ts)_
|
|
1193
1193
|
|
|
1194
1194
|
## `qfg log`
|
|
1195
1195
|
|
|
@@ -1214,7 +1214,7 @@ EXAMPLES
|
|
|
1214
1214
|
$ qfg log --limit 5
|
|
1215
1215
|
```
|
|
1216
1216
|
|
|
1217
|
-
_See code: [src/commands/log.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1217
|
+
_See code: [src/commands/log.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/log.ts)_
|
|
1218
1218
|
|
|
1219
1219
|
## `qfg log-level NAME`
|
|
1220
1220
|
|
|
@@ -1279,7 +1279,7 @@ EXAMPLES
|
|
|
1279
1279
|
$ qfg log-level log-level.my-app --target=Chatty --value=INFO --environment=production
|
|
1280
1280
|
```
|
|
1281
1281
|
|
|
1282
|
-
_See code: [src/commands/log-level.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1282
|
+
_See code: [src/commands/log-level.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/log-level.ts)_
|
|
1283
1283
|
|
|
1284
1284
|
## `qfg login`
|
|
1285
1285
|
|
|
@@ -1304,7 +1304,7 @@ EXAMPLES
|
|
|
1304
1304
|
$ qfg login --profile myprofile
|
|
1305
1305
|
```
|
|
1306
1306
|
|
|
1307
|
-
_See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1307
|
+
_See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/login.ts)_
|
|
1308
1308
|
|
|
1309
1309
|
## `qfg logout`
|
|
1310
1310
|
|
|
@@ -1327,7 +1327,7 @@ EXAMPLES
|
|
|
1327
1327
|
$ qfg logout
|
|
1328
1328
|
```
|
|
1329
1329
|
|
|
1330
|
-
_See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1330
|
+
_See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/logout.ts)_
|
|
1331
1331
|
|
|
1332
1332
|
## `qfg migrate`
|
|
1333
1333
|
|
|
@@ -1392,7 +1392,7 @@ EXAMPLES
|
|
|
1392
1392
|
$ qfg migrate --from launch --source-api-key $LAUNCH_API_KEY --dir ./quonfig-repo --staging
|
|
1393
1393
|
```
|
|
1394
1394
|
|
|
1395
|
-
_See code: [src/commands/migrate.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1395
|
+
_See code: [src/commands/migrate.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/migrate.ts)_
|
|
1396
1396
|
|
|
1397
1397
|
## `qfg migrate doctor`
|
|
1398
1398
|
|
|
@@ -1430,7 +1430,7 @@ EXAMPLES
|
|
|
1430
1430
|
$ qfg migrate doctor --json
|
|
1431
1431
|
```
|
|
1432
1432
|
|
|
1433
|
-
_See code: [src/commands/migrate/doctor.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1433
|
+
_See code: [src/commands/migrate/doctor.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/migrate/doctor.ts)_
|
|
1434
1434
|
|
|
1435
1435
|
## `qfg migrate my-code`
|
|
1436
1436
|
|
|
@@ -1462,7 +1462,7 @@ EXAMPLES
|
|
|
1462
1462
|
$ qfg migrate my-code --dry-run
|
|
1463
1463
|
```
|
|
1464
1464
|
|
|
1465
|
-
_See code: [src/commands/migrate/my-code.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1465
|
+
_See code: [src/commands/migrate/my-code.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/migrate/my-code.ts)_
|
|
1466
1466
|
|
|
1467
1467
|
## `qfg migrate status`
|
|
1468
1468
|
|
|
@@ -1493,7 +1493,7 @@ EXAMPLES
|
|
|
1493
1493
|
$ qfg migrate status --json
|
|
1494
1494
|
```
|
|
1495
1495
|
|
|
1496
|
-
_See code: [src/commands/migrate/status.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1496
|
+
_See code: [src/commands/migrate/status.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/migrate/status.ts)_
|
|
1497
1497
|
|
|
1498
1498
|
## `qfg override [NAME] [VALUE]`
|
|
1499
1499
|
|
|
@@ -1548,7 +1548,7 @@ EXAMPLES
|
|
|
1548
1548
|
$ qfg override my.flag true --env=staging
|
|
1549
1549
|
```
|
|
1550
1550
|
|
|
1551
|
-
_See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1551
|
+
_See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/override.ts)_
|
|
1552
1552
|
|
|
1553
1553
|
## `qfg pull`
|
|
1554
1554
|
|
|
@@ -1595,7 +1595,7 @@ EXAMPLES
|
|
|
1595
1595
|
$ qfg pull # uses QUONFIG_DIR env var
|
|
1596
1596
|
```
|
|
1597
1597
|
|
|
1598
|
-
_See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1598
|
+
_See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/pull.ts)_
|
|
1599
1599
|
|
|
1600
1600
|
## `qfg push`
|
|
1601
1601
|
|
|
@@ -1644,7 +1644,7 @@ EXAMPLES
|
|
|
1644
1644
|
$ qfg push --dir ./our-config --yes
|
|
1645
1645
|
```
|
|
1646
1646
|
|
|
1647
|
-
_See code: [src/commands/push.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1647
|
+
_See code: [src/commands/push.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/push.ts)_
|
|
1648
1648
|
|
|
1649
1649
|
## `qfg run [COMMAND]`
|
|
1650
1650
|
|
|
@@ -1695,7 +1695,7 @@ EXAMPLES
|
|
|
1695
1695
|
$ qfg run --env DATABASE_URL=db.url --preserve-env --environment=staging -- npm test
|
|
1696
1696
|
```
|
|
1697
1697
|
|
|
1698
|
-
_See code: [src/commands/run.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1698
|
+
_See code: [src/commands/run.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/run.ts)_
|
|
1699
1699
|
|
|
1700
1700
|
## `qfg schema NAME`
|
|
1701
1701
|
|
|
@@ -1733,7 +1733,7 @@ EXAMPLES
|
|
|
1733
1733
|
$ qfg schema my-schema --set-json-schema=@schemas/my-schema.json --protected
|
|
1734
1734
|
```
|
|
1735
1735
|
|
|
1736
|
-
_See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1736
|
+
_See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/schema.ts)_
|
|
1737
1737
|
|
|
1738
1738
|
## `qfg sdk-key`
|
|
1739
1739
|
|
|
@@ -1760,7 +1760,7 @@ EXAMPLES
|
|
|
1760
1760
|
$ qfg sdk-key revoke <key-id>
|
|
1761
1761
|
```
|
|
1762
1762
|
|
|
1763
|
-
_See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1763
|
+
_See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/sdk-key.ts)_
|
|
1764
1764
|
|
|
1765
1765
|
## `qfg sdk-key create`
|
|
1766
1766
|
|
|
@@ -1792,7 +1792,7 @@ EXAMPLES
|
|
|
1792
1792
|
$ qfg sdk-key create --environment staging --type browser
|
|
1793
1793
|
```
|
|
1794
1794
|
|
|
1795
|
-
_See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1795
|
+
_See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/sdk-key/create.ts)_
|
|
1796
1796
|
|
|
1797
1797
|
## `qfg sdk-key list`
|
|
1798
1798
|
|
|
@@ -1821,7 +1821,7 @@ EXAMPLES
|
|
|
1821
1821
|
$ qfg sdk-key list --environment production
|
|
1822
1822
|
```
|
|
1823
1823
|
|
|
1824
|
-
_See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1824
|
+
_See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/sdk-key/list.ts)_
|
|
1825
1825
|
|
|
1826
1826
|
## `qfg sdk-key revoke KEYID`
|
|
1827
1827
|
|
|
@@ -1848,7 +1848,7 @@ EXAMPLES
|
|
|
1848
1848
|
$ qfg sdk-key revoke a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
|
1849
1849
|
```
|
|
1850
1850
|
|
|
1851
|
-
_See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1851
|
+
_See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/sdk-key/revoke.ts)_
|
|
1852
1852
|
|
|
1853
1853
|
## `qfg serve`
|
|
1854
1854
|
|
|
@@ -1906,7 +1906,7 @@ EXAMPLES
|
|
|
1906
1906
|
$ qfg serve --port 6581 --frontend-sdk-key PUBLIC_KEY
|
|
1907
1907
|
```
|
|
1908
1908
|
|
|
1909
|
-
_See code: [src/commands/serve.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1909
|
+
_See code: [src/commands/serve.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/serve.ts)_
|
|
1910
1910
|
|
|
1911
1911
|
## `qfg set-default [NAME]`
|
|
1912
1912
|
|
|
@@ -1982,7 +1982,7 @@ EXAMPLES
|
|
|
1982
1982
|
$ qfg pull && qfg config-schema
|
|
1983
1983
|
```
|
|
1984
1984
|
|
|
1985
|
-
_See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
1985
|
+
_See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/set-default.ts)_
|
|
1986
1986
|
|
|
1987
1987
|
## `qfg set-rollout [NAME]`
|
|
1988
1988
|
|
|
@@ -2073,7 +2073,7 @@ EXAMPLES
|
|
|
2073
2073
|
$ qfg set-rollout my.variant.flag --environment production --weights "a:33,b:33,c:34" --hash-by user.id
|
|
2074
2074
|
```
|
|
2075
2075
|
|
|
2076
|
-
_See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
2076
|
+
_See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/set-rollout.ts)_
|
|
2077
2077
|
|
|
2078
2078
|
## `qfg sync`
|
|
2079
2079
|
|
|
@@ -2105,7 +2105,7 @@ EXAMPLES
|
|
|
2105
2105
|
$ qfg sync --watch --dir ./our-config --interval 30
|
|
2106
2106
|
```
|
|
2107
2107
|
|
|
2108
|
-
_See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
2108
|
+
_See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/sync.ts)_
|
|
2109
2109
|
|
|
2110
2110
|
## `qfg toggle [NAME]`
|
|
2111
2111
|
|
|
@@ -2212,7 +2212,7 @@ EXAMPLES
|
|
|
2212
2212
|
$ qfg verify --json
|
|
2213
2213
|
```
|
|
2214
2214
|
|
|
2215
|
-
_See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
2215
|
+
_See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/verify.ts)_
|
|
2216
2216
|
|
|
2217
2217
|
## `qfg whoami`
|
|
2218
2218
|
|
|
@@ -2235,7 +2235,7 @@ EXAMPLES
|
|
|
2235
2235
|
$ qfg whoami
|
|
2236
2236
|
```
|
|
2237
2237
|
|
|
2238
|
-
_See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
2238
|
+
_See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/whoami.ts)_
|
|
2239
2239
|
|
|
2240
2240
|
## `qfg workspace`
|
|
2241
2241
|
|
|
@@ -2258,7 +2258,7 @@ EXAMPLES
|
|
|
2258
2258
|
$ qfg workspace
|
|
2259
2259
|
```
|
|
2260
2260
|
|
|
2261
|
-
_See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
2261
|
+
_See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/workspace.ts)_
|
|
2262
2262
|
|
|
2263
2263
|
## `qfg workspace bootstrap`
|
|
2264
2264
|
|
|
@@ -2291,7 +2291,7 @@ EXAMPLES
|
|
|
2291
2291
|
$ qfg workspace bootstrap --dir ./our-config --skip-validate
|
|
2292
2292
|
```
|
|
2293
2293
|
|
|
2294
|
-
_See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
2294
|
+
_See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/workspace/bootstrap.ts)_
|
|
2295
2295
|
|
|
2296
2296
|
## `qfg workspace create SLUG`
|
|
2297
2297
|
|
|
@@ -2325,7 +2325,7 @@ EXAMPLES
|
|
|
2325
2325
|
$ qfg workspace create my-team --org 11111111-1111-1111-1111-111111111111
|
|
2326
2326
|
```
|
|
2327
2327
|
|
|
2328
|
-
_See code: [src/commands/workspace/create.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
2328
|
+
_See code: [src/commands/workspace/create.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/workspace/create.ts)_
|
|
2329
2329
|
|
|
2330
2330
|
## `qfg workspace switch [SLUG]`
|
|
2331
2331
|
|
|
@@ -2353,7 +2353,7 @@ EXAMPLES
|
|
|
2353
2353
|
$ qfg workspace switch acme/production
|
|
2354
2354
|
```
|
|
2355
2355
|
|
|
2356
|
-
_See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.2.
|
|
2356
|
+
_See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.2.2/src/commands/workspace/switch.ts)_
|
|
2357
2357
|
<!-- commandsstop -->
|
|
2358
2358
|
|
|
2359
2359
|
## Local Development
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integer map key order warning (qfg-e87r.21).
|
|
3
|
+
*
|
|
4
|
+
* JavaScript objects always put integer keys first, ascending, then every
|
|
5
|
+
* other key in insertion order. The app editor reads a value with JSON.parse
|
|
6
|
+
* and saves it with JSON.stringify, so a hand-written map
|
|
7
|
+
* `{"b": 2, "a": 1, "5": 0}` comes back from an app save as
|
|
8
|
+
* `{"5": 0, "b": 2, "a": 1}`. Nothing is lost, but git gets a one-time
|
|
9
|
+
* key-order diff. This check warns about it up front.
|
|
10
|
+
*
|
|
11
|
+
* Scope: maps only, meaning objects under an `additionalProperties` schema
|
|
12
|
+
* (keys the author chooses). Objects with only schema-fixed `properties` are
|
|
13
|
+
* skipped. A map is checked only if it has at least one integer key, and the
|
|
14
|
+
* file text is scanned for the real key order (JSON.parse already reorders)
|
|
15
|
+
* only if some map has one.
|
|
16
|
+
*
|
|
17
|
+
* Lives inside cli/src/verify/ because the app-gitea hook build only copies
|
|
18
|
+
* this directory (see validate.ts). No dependencies on purpose.
|
|
19
|
+
*/
|
|
20
|
+
type Segment = number | string;
|
|
21
|
+
type Schema = Record<string, unknown>;
|
|
22
|
+
export interface KeyOrderWarning {
|
|
23
|
+
integerKeys: string[];
|
|
24
|
+
path: string;
|
|
25
|
+
}
|
|
26
|
+
/** True for keys JavaScript moves to the front: "0", "5", "1001"; not "007", "-1", "1.5". */
|
|
27
|
+
export declare function isIntegerKey(key: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Find maps in `value` (walked with `schema`) whose key order in the file
|
|
30
|
+
* would change on an app save. `readOrderedValue` returns `value` as written
|
|
31
|
+
* in the file (see scanOrderedJson); it is only called when some map has an
|
|
32
|
+
* integer key, so maps without one cost nothing extra.
|
|
33
|
+
*/
|
|
34
|
+
export declare function findMapKeyOrderWarnings(schema: Schema, value: unknown, readOrderedValue: () => OrderedNode | undefined, basePath: string): KeyOrderWarning[];
|
|
35
|
+
export type OrderedNode = {
|
|
36
|
+
entries: Map<string, OrderedNode>;
|
|
37
|
+
keys: string[];
|
|
38
|
+
kind: 'object';
|
|
39
|
+
} | {
|
|
40
|
+
items: OrderedNode[];
|
|
41
|
+
kind: 'array';
|
|
42
|
+
} | {
|
|
43
|
+
kind: 'scalar';
|
|
44
|
+
};
|
|
45
|
+
export declare function navigate(node: OrderedNode, segments: Segment[]): OrderedNode | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Parse JSON text keeping each object's keys in file order. Duplicate keys
|
|
48
|
+
* keep their first position, as JavaScript does. Returns undefined on text
|
|
49
|
+
* that is not JSON (JSON.parse has already reported it).
|
|
50
|
+
*/
|
|
51
|
+
export declare function scanOrderedJson(text: string): OrderedNode | undefined;
|
|
52
|
+
export {};
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integer map key order warning (qfg-e87r.21).
|
|
3
|
+
*
|
|
4
|
+
* JavaScript objects always put integer keys first, ascending, then every
|
|
5
|
+
* other key in insertion order. The app editor reads a value with JSON.parse
|
|
6
|
+
* and saves it with JSON.stringify, so a hand-written map
|
|
7
|
+
* `{"b": 2, "a": 1, "5": 0}` comes back from an app save as
|
|
8
|
+
* `{"5": 0, "b": 2, "a": 1}`. Nothing is lost, but git gets a one-time
|
|
9
|
+
* key-order diff. This check warns about it up front.
|
|
10
|
+
*
|
|
11
|
+
* Scope: maps only, meaning objects under an `additionalProperties` schema
|
|
12
|
+
* (keys the author chooses). Objects with only schema-fixed `properties` are
|
|
13
|
+
* skipped. A map is checked only if it has at least one integer key, and the
|
|
14
|
+
* file text is scanned for the real key order (JSON.parse already reorders)
|
|
15
|
+
* only if some map has one.
|
|
16
|
+
*
|
|
17
|
+
* Lives inside cli/src/verify/ because the app-gitea hook build only copies
|
|
18
|
+
* this directory (see validate.ts). No dependencies on purpose.
|
|
19
|
+
*/
|
|
20
|
+
/** Largest array index in JavaScript: 2^32 - 2. */
|
|
21
|
+
const MAX_INTEGER_KEY = 4_294_967_294;
|
|
22
|
+
/** True for keys JavaScript moves to the front: "0", "5", "1001"; not "007", "-1", "1.5". */
|
|
23
|
+
export function isIntegerKey(key) {
|
|
24
|
+
return /^(?:0|[1-9]\d*)$/.test(key) && Number(key) <= MAX_INTEGER_KEY;
|
|
25
|
+
}
|
|
26
|
+
const isPlainObject = (node) => typeof node === 'object' && node !== null && !Array.isArray(node);
|
|
27
|
+
/**
|
|
28
|
+
* Find maps in `value` (walked with `schema`) whose key order in the file
|
|
29
|
+
* would change on an app save. `readOrderedValue` returns `value` as written
|
|
30
|
+
* in the file (see scanOrderedJson); it is only called when some map has an
|
|
31
|
+
* integer key, so maps without one cost nothing extra.
|
|
32
|
+
*/
|
|
33
|
+
export function findMapKeyOrderWarnings(schema, value, readOrderedValue, basePath) {
|
|
34
|
+
const candidates = new Map();
|
|
35
|
+
const walk = (node, nodeSchema, segments, depth) => {
|
|
36
|
+
if (depth > 64)
|
|
37
|
+
return;
|
|
38
|
+
const schemas = expandSchema(schema, nodeSchema);
|
|
39
|
+
if (schemas.length === 0)
|
|
40
|
+
return;
|
|
41
|
+
if (Array.isArray(node)) {
|
|
42
|
+
for (const [index, child] of node.entries()) {
|
|
43
|
+
for (const s of schemas)
|
|
44
|
+
walk(child, itemSchema(s, index), [...segments, index], depth + 1);
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!isPlainObject(node))
|
|
49
|
+
return;
|
|
50
|
+
const keys = Object.keys(node);
|
|
51
|
+
if (schemas.some((s) => isMapSchema(s)) && keys.some((k) => isIntegerKey(k))) {
|
|
52
|
+
candidates.set(formatPath(basePath, segments), segments);
|
|
53
|
+
}
|
|
54
|
+
for (const key of keys) {
|
|
55
|
+
for (const s of schemas) {
|
|
56
|
+
const child = propertySchema(s, key);
|
|
57
|
+
if (child !== undefined)
|
|
58
|
+
walk(node[key], child, [...segments, key], depth + 1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
walk(value, schema, [], 0);
|
|
63
|
+
if (candidates.size === 0)
|
|
64
|
+
return [];
|
|
65
|
+
const ordered = readOrderedValue();
|
|
66
|
+
if (!ordered)
|
|
67
|
+
return [];
|
|
68
|
+
const warnings = [];
|
|
69
|
+
for (const [path, segments] of candidates) {
|
|
70
|
+
const node = navigate(ordered, segments);
|
|
71
|
+
if (node?.kind !== 'object')
|
|
72
|
+
continue;
|
|
73
|
+
const keys = node.keys;
|
|
74
|
+
const integerKeys = keys.filter((k) => isIntegerKey(k));
|
|
75
|
+
const saved = [...integerKeys].sort((a, b) => Number(a) - Number(b));
|
|
76
|
+
saved.push(...keys.filter((k) => !isIntegerKey(k)));
|
|
77
|
+
if (saved.every((k, i) => k === keys[i]))
|
|
78
|
+
continue;
|
|
79
|
+
warnings.push({ integerKeys, path });
|
|
80
|
+
}
|
|
81
|
+
return warnings.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
82
|
+
}
|
|
83
|
+
// ── Schema navigation ───────────────────────────────────────────────────
|
|
84
|
+
function isMapSchema(schema) {
|
|
85
|
+
return schema.additionalProperties === true || isPlainObject(schema.additionalProperties);
|
|
86
|
+
}
|
|
87
|
+
/** Resolve local `$ref`s and flatten allOf/anyOf/oneOf into the schemas that apply. */
|
|
88
|
+
function expandSchema(root, schema, seen = new Set()) {
|
|
89
|
+
if (!isPlainObject(schema) || seen.has(schema))
|
|
90
|
+
return [];
|
|
91
|
+
seen.add(schema);
|
|
92
|
+
const out = [schema];
|
|
93
|
+
if (typeof schema.$ref === 'string')
|
|
94
|
+
out.push(...expandSchema(root, resolveRef(root, schema.$ref), seen));
|
|
95
|
+
for (const keyword of ['allOf', 'anyOf', 'oneOf']) {
|
|
96
|
+
const branches = schema[keyword];
|
|
97
|
+
if (Array.isArray(branches))
|
|
98
|
+
for (const branch of branches)
|
|
99
|
+
out.push(...expandSchema(root, branch, seen));
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
function resolveRef(root, ref) {
|
|
104
|
+
if (!ref.startsWith('#'))
|
|
105
|
+
return undefined;
|
|
106
|
+
let node = root;
|
|
107
|
+
for (const raw of ref.slice(1).split('/').filter(Boolean)) {
|
|
108
|
+
const part = decodeURIComponent(raw).replaceAll('~1', '/').replaceAll('~0', '~');
|
|
109
|
+
if (!isPlainObject(node))
|
|
110
|
+
return undefined;
|
|
111
|
+
node = node[part];
|
|
112
|
+
}
|
|
113
|
+
return node;
|
|
114
|
+
}
|
|
115
|
+
function propertySchema(schema, key) {
|
|
116
|
+
const properties = schema.properties;
|
|
117
|
+
if (isPlainObject(properties) && key in properties)
|
|
118
|
+
return properties[key];
|
|
119
|
+
return isMapSchema(schema) ? schema.additionalProperties : undefined;
|
|
120
|
+
}
|
|
121
|
+
function itemSchema(schema, index) {
|
|
122
|
+
if (Array.isArray(schema.prefixItems) && index < schema.prefixItems.length)
|
|
123
|
+
return schema.prefixItems[index];
|
|
124
|
+
// draft-07 tuple form
|
|
125
|
+
if (Array.isArray(schema.items))
|
|
126
|
+
return index < schema.items.length ? schema.items[index] : schema.additionalItems;
|
|
127
|
+
return schema.items;
|
|
128
|
+
}
|
|
129
|
+
function formatPath(base, segments) {
|
|
130
|
+
let out = base;
|
|
131
|
+
for (const segment of segments)
|
|
132
|
+
out += typeof segment === 'number' ? `[${segment}]` : `.${segment}`;
|
|
133
|
+
return out;
|
|
134
|
+
}
|
|
135
|
+
export function navigate(node, segments) {
|
|
136
|
+
let current = node;
|
|
137
|
+
for (const segment of segments) {
|
|
138
|
+
if (current?.kind === 'object' && typeof segment === 'string')
|
|
139
|
+
current = current.entries.get(segment);
|
|
140
|
+
else if (current?.kind === 'array' && typeof segment === 'number')
|
|
141
|
+
current = current.items[segment];
|
|
142
|
+
else
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
return current;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Parse JSON text keeping each object's keys in file order. Duplicate keys
|
|
149
|
+
* keep their first position, as JavaScript does. Returns undefined on text
|
|
150
|
+
* that is not JSON (JSON.parse has already reported it).
|
|
151
|
+
*/
|
|
152
|
+
export function scanOrderedJson(text) {
|
|
153
|
+
let pos = 0;
|
|
154
|
+
const skipWhitespace = () => {
|
|
155
|
+
while (pos < text.length && ' \t\n\r'.includes(text[pos]))
|
|
156
|
+
pos++;
|
|
157
|
+
};
|
|
158
|
+
const readString = () => {
|
|
159
|
+
const start = pos;
|
|
160
|
+
pos++; // opening quote
|
|
161
|
+
while (pos < text.length && text[pos] !== '"')
|
|
162
|
+
pos += text[pos] === '\\' ? 2 : 1;
|
|
163
|
+
if (pos >= text.length)
|
|
164
|
+
throw new Error('unterminated string');
|
|
165
|
+
pos++; // closing quote
|
|
166
|
+
return JSON.parse(text.slice(start, pos));
|
|
167
|
+
};
|
|
168
|
+
const readValue = () => {
|
|
169
|
+
skipWhitespace();
|
|
170
|
+
const ch = text[pos];
|
|
171
|
+
if (ch === '{') {
|
|
172
|
+
pos++;
|
|
173
|
+
const keys = [];
|
|
174
|
+
const entries = new Map();
|
|
175
|
+
skipWhitespace();
|
|
176
|
+
if (text[pos] === '}') {
|
|
177
|
+
pos++;
|
|
178
|
+
return { entries, keys, kind: 'object' };
|
|
179
|
+
}
|
|
180
|
+
for (;;) {
|
|
181
|
+
skipWhitespace();
|
|
182
|
+
if (text[pos] !== '"')
|
|
183
|
+
throw new Error('expected key');
|
|
184
|
+
const key = readString();
|
|
185
|
+
skipWhitespace();
|
|
186
|
+
if (text[pos] !== ':')
|
|
187
|
+
throw new Error('expected colon');
|
|
188
|
+
pos++;
|
|
189
|
+
const child = readValue();
|
|
190
|
+
if (!entries.has(key))
|
|
191
|
+
keys.push(key);
|
|
192
|
+
entries.set(key, child);
|
|
193
|
+
skipWhitespace();
|
|
194
|
+
if (text[pos] === ',') {
|
|
195
|
+
pos++;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
if (text[pos] === '}') {
|
|
199
|
+
pos++;
|
|
200
|
+
return { entries, keys, kind: 'object' };
|
|
201
|
+
}
|
|
202
|
+
throw new Error('expected , or }');
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (ch === '[') {
|
|
206
|
+
pos++;
|
|
207
|
+
const items = [];
|
|
208
|
+
skipWhitespace();
|
|
209
|
+
if (text[pos] === ']') {
|
|
210
|
+
pos++;
|
|
211
|
+
return { items, kind: 'array' };
|
|
212
|
+
}
|
|
213
|
+
for (;;) {
|
|
214
|
+
items.push(readValue());
|
|
215
|
+
skipWhitespace();
|
|
216
|
+
if (text[pos] === ',') {
|
|
217
|
+
pos++;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (text[pos] === ']') {
|
|
221
|
+
pos++;
|
|
222
|
+
return { items, kind: 'array' };
|
|
223
|
+
}
|
|
224
|
+
throw new Error('expected , or ]');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (ch === '"') {
|
|
228
|
+
readString();
|
|
229
|
+
return { kind: 'scalar' };
|
|
230
|
+
}
|
|
231
|
+
const start = pos;
|
|
232
|
+
while (pos < text.length && !',}] \t\n\r'.includes(text[pos]))
|
|
233
|
+
pos++;
|
|
234
|
+
if (pos === start)
|
|
235
|
+
throw new Error('expected value');
|
|
236
|
+
return { kind: 'scalar' };
|
|
237
|
+
};
|
|
238
|
+
try {
|
|
239
|
+
const node = readValue();
|
|
240
|
+
skipWhitespace();
|
|
241
|
+
return pos === text.length ? node : undefined;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
* collide in the shared Ajv instance.
|
|
16
16
|
*
|
|
17
17
|
* Lives inside cli/src/verify/ because the app-gitea hook build only copies
|
|
18
|
-
* this directory (see validate.ts). The hook
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* this directory (see validate.ts). The hook runs value validation
|
|
19
|
+
* (qfg-q5f6.10), so app-gitea/Dockerfile must install `ajv` and
|
|
20
|
+
* `ajv-formats`.
|
|
21
21
|
*
|
|
22
22
|
* Imports: the CLI is ESM with nodenext resolution and Ajv ships CommonJS, so
|
|
23
23
|
* the default import may be the class or the module object depending on the
|
|
@@ -97,7 +97,10 @@ export function runHookChecks(refs, opts) {
|
|
|
97
97
|
log(`qfg-verify: validating ${ref.refName} (${ref.newOid.slice(0, 8)})`);
|
|
98
98
|
try {
|
|
99
99
|
const files = readFilesFromCommit(ref.newOid, cwd);
|
|
100
|
-
|
|
100
|
+
// validateValues: schema-bound JSON values must match their schema and
|
|
101
|
+
// every schema must compile, same as the CLI and the app (qfg-q5f6.10,
|
|
102
|
+
// plan 2026-09-25-advanced-schema-for-jev.md W1b Decision 10).
|
|
103
|
+
const result = validateFileMap(files, { validateValues: true });
|
|
101
104
|
log(formatResult(result));
|
|
102
105
|
if (!result.valid) {
|
|
103
106
|
hasErrors = true;
|
|
@@ -255,28 +258,57 @@ main is append-only: every push must fast-forward from the current tip. What to
|
|
|
255
258
|
export function readFilesFromCommit(commitOid, cwd) {
|
|
256
259
|
const files = new Map();
|
|
257
260
|
const dirs = ['configs', 'feature-flags', 'segments', 'log-levels', 'schemas', 'schemas-protected'];
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
261
|
+
// qfg-3nyi: exactly two git processes per call, whatever the tree size. One
|
|
262
|
+
// `git show` per file cost ~10s on a 620-file workspace, all of it spawns.
|
|
263
|
+
//
|
|
264
|
+
// List every leaf entry under the validated dirs at the given commit. A dir
|
|
265
|
+
// that doesn't exist yields nothing (exit 0); a bad/unreadable OID throws —
|
|
266
|
+
// fail closed, the hook rejects the push.
|
|
267
|
+
const listing = execFileSync('git', ['ls-tree', '-z', '-r', commitOid, '--', ...dirs.map((dir) => `${dir}/`)], {
|
|
268
|
+
cwd,
|
|
269
|
+
encoding: 'utf8',
|
|
270
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
271
|
+
});
|
|
272
|
+
// Each entry is `<mode> SP <type> SP <oid> TAB <path>`.
|
|
273
|
+
const entries = listing
|
|
274
|
+
.split('\0')
|
|
275
|
+
.filter(Boolean)
|
|
276
|
+
.map((entry) => {
|
|
277
|
+
const tab = entry.indexOf('\t');
|
|
278
|
+
const [, type, oid] = entry.slice(0, tab).split(' ');
|
|
279
|
+
return { filePath: entry.slice(tab + 1), oid, type };
|
|
280
|
+
});
|
|
281
|
+
// Fail closed: an entry that isn't a readable blob (submodule gitlink, bad
|
|
282
|
+
// object, ...) must not be accepted unvalidated. Throwing reaches the
|
|
283
|
+
// hook's per-ref handler, which rejects the push.
|
|
284
|
+
for (const { filePath, type } of entries) {
|
|
285
|
+
if (type !== 'blob')
|
|
286
|
+
throw new Error(`cannot read ${filePath}: tree entry is a ${type}, not a file`);
|
|
287
|
+
}
|
|
288
|
+
if (entries.length === 0)
|
|
289
|
+
return files;
|
|
290
|
+
const output = execFileSync('git', ['cat-file', '--batch'], {
|
|
291
|
+
cwd,
|
|
292
|
+
input: entries.map(({ oid }) => oid).join('\n') + '\n',
|
|
293
|
+
// The whole tree comes back in one buffer; execFileSync's 1MB default
|
|
294
|
+
// would fail a large workspace.
|
|
295
|
+
maxBuffer: 1024 ** 3,
|
|
296
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
297
|
+
});
|
|
298
|
+
// Replies come in request order: `<oid> SP blob SP <size> LF <bytes> LF`, or
|
|
299
|
+
// `<oid> SP missing LF`. Sizes are bytes, so slice the Buffer before decoding.
|
|
300
|
+
let offset = 0;
|
|
301
|
+
for (const { filePath, oid } of entries) {
|
|
302
|
+
const headerEnd = output.indexOf(0x0a, offset);
|
|
303
|
+
const header = headerEnd === -1 ? '' : output.subarray(offset, headerEnd).toString('utf8');
|
|
304
|
+
const [replyOid, replyType, size] = header.split(' ');
|
|
305
|
+
if (replyOid !== oid || replyType !== 'blob') {
|
|
306
|
+
throw new Error(`cannot read ${filePath}: git cat-file replied "${header}"`);
|
|
279
307
|
}
|
|
308
|
+
const start = headerEnd + 1;
|
|
309
|
+
const end = start + Number(size);
|
|
310
|
+
files.set(filePath, output.subarray(start, end).toString('utf8'));
|
|
311
|
+
offset = end + 1;
|
|
280
312
|
}
|
|
281
313
|
return files;
|
|
282
314
|
}
|
|
@@ -16,8 +16,11 @@
|
|
|
16
16
|
* - Log level constraints (valueType=log_level)
|
|
17
17
|
* - Referential integrity (IN_SEG/NOT_IN_SEG reference existing segments)
|
|
18
18
|
* - schemaKey references existing schemas
|
|
19
|
-
* - Schema-bound JSON values match their schema
|
|
20
|
-
* `validateValues
|
|
19
|
+
* - Schema-bound JSON values match their schema, and every schema compiles
|
|
20
|
+
* (Ajv, opt-in via `validateValues`, which the CLI and the pre-receive
|
|
21
|
+
* hook both set; see validateBoundJsonValues)
|
|
22
|
+
* - Warning: a schema map whose integer keys an app save would move to the
|
|
23
|
+
* front (same gate; see warnMapKeyOrder)
|
|
21
24
|
* - Rule structure (criteria + value present)
|
|
22
25
|
* - Value type consistency
|
|
23
26
|
* - Weighted values non-empty and consistent
|
|
@@ -46,10 +49,10 @@ export interface ValidationStats {
|
|
|
46
49
|
export interface ValidateOptions {
|
|
47
50
|
/**
|
|
48
51
|
* Validate every schema-bound JSON value against its schema with Ajv
|
|
49
|
-
* (qfg-phcv). The CLI callers (`qfg
|
|
50
|
-
* bootstrap`, `qfg migrate --push`)
|
|
51
|
-
* (standalone.ts
|
|
52
|
-
* 2026-09-25-advanced-schema-for-jev.md W1b).
|
|
52
|
+
* (qfg-phcv), and check that every schema compiles. The CLI callers (`qfg
|
|
53
|
+
* verify`, `qfg push`, `qfg workspace bootstrap`, `qfg migrate --push`) and
|
|
54
|
+
* the pre-receive hook (standalone.ts, since qfg-q5f6.10 / plan
|
|
55
|
+
* 2026-09-25-advanced-schema-for-jev.md W1b) all turn it on.
|
|
53
56
|
*/
|
|
54
57
|
validateValues?: boolean;
|
|
55
58
|
}
|
package/dist/verify/validate.js
CHANGED
|
@@ -16,13 +16,17 @@
|
|
|
16
16
|
* - Log level constraints (valueType=log_level)
|
|
17
17
|
* - Referential integrity (IN_SEG/NOT_IN_SEG reference existing segments)
|
|
18
18
|
* - schemaKey references existing schemas
|
|
19
|
-
* - Schema-bound JSON values match their schema
|
|
20
|
-
* `validateValues
|
|
19
|
+
* - Schema-bound JSON values match their schema, and every schema compiles
|
|
20
|
+
* (Ajv, opt-in via `validateValues`, which the CLI and the pre-receive
|
|
21
|
+
* hook both set; see validateBoundJsonValues)
|
|
22
|
+
* - Warning: a schema map whose integer keys an app save would move to the
|
|
23
|
+
* front (same gate; see warnMapKeyOrder)
|
|
21
24
|
* - Rule structure (criteria + value present)
|
|
22
25
|
* - Value type consistency
|
|
23
26
|
* - Weighted values non-empty and consistent
|
|
24
27
|
*/
|
|
25
28
|
import { z } from 'zod';
|
|
29
|
+
import { findMapKeyOrderWarnings, navigate, scanOrderedJson } from './map-key-order.js';
|
|
26
30
|
import { compileSchema, validateAgainstSchema } from './schema-validator.js';
|
|
27
31
|
// Inlined to keep this directory self-contained for the standalone bun-compile
|
|
28
32
|
// build that runs as the qfg-verify pre-receive hook in app-gitea — that build
|
|
@@ -574,6 +578,7 @@ export function validateWorkspace(workspaceDir, options = {}) {
|
|
|
574
578
|
}
|
|
575
579
|
// Second pass: referential integrity. Keep the plain skip-filter here —
|
|
576
580
|
// ghost entries were already reported (once) by the first pass.
|
|
581
|
+
const cycleNodes = [];
|
|
577
582
|
for (const dir of KNOWN_DIRS) {
|
|
578
583
|
const dirPath = path.join(workspaceDir, dir);
|
|
579
584
|
if (!fs.existsSync(dirPath))
|
|
@@ -603,6 +608,7 @@ export function validateWorkspace(workspaceDir, options = {}) {
|
|
|
603
608
|
if (!result.success)
|
|
604
609
|
continue;
|
|
605
610
|
const config = result.data;
|
|
611
|
+
cycleNodes.push({ config, raw: parsed, relPath });
|
|
606
612
|
// Check segment references in criteria
|
|
607
613
|
const segRefs = collectSegmentReferences(config);
|
|
608
614
|
for (const ref of segRefs) {
|
|
@@ -628,8 +634,10 @@ export function validateWorkspace(workspaceDir, options = {}) {
|
|
|
628
634
|
});
|
|
629
635
|
}
|
|
630
636
|
}
|
|
631
|
-
if (options.validateValues)
|
|
637
|
+
if (options.validateValues) {
|
|
632
638
|
validateBoundJsonValues(parsed, config, schemaDocuments, relPath, issues);
|
|
639
|
+
warnMapKeyOrder(raw, parsed, config, schemaDocuments, relPath, issues);
|
|
640
|
+
}
|
|
633
641
|
// Check environment IDs referenced in config exist in quonfig.json
|
|
634
642
|
if (declaredEnvIds.size > 0) {
|
|
635
643
|
for (const env of config.environments) {
|
|
@@ -646,6 +654,7 @@ export function validateWorkspace(workspaceDir, options = {}) {
|
|
|
646
654
|
}
|
|
647
655
|
}
|
|
648
656
|
}
|
|
657
|
+
detectReferenceCycles(cycleNodes, issues);
|
|
649
658
|
// Check for duplicate keys across directories (case-insensitive — see
|
|
650
659
|
// detectDuplicateKeys).
|
|
651
660
|
const uniqueConfigKeys = detectDuplicateKeys(allConfigs, 'key', issues);
|
|
@@ -862,8 +871,10 @@ export function validateFileMap(files, options = {}) {
|
|
|
862
871
|
suggestion: `Create schemas/${config.schemaKey}.json or schemas-protected/${config.schemaKey}.json, or remove schemaKey`,
|
|
863
872
|
});
|
|
864
873
|
}
|
|
865
|
-
if (options.validateValues)
|
|
874
|
+
if (options.validateValues) {
|
|
866
875
|
validateBoundJsonValues(raw, config, schemaDocuments, relPath, issues);
|
|
876
|
+
warnMapKeyOrder(files.get(relPath) ?? '', raw, config, schemaDocuments, relPath, issues);
|
|
877
|
+
}
|
|
867
878
|
// Check environment IDs referenced in config exist in quonfig.json
|
|
868
879
|
if (declaredEnvIds.size > 0) {
|
|
869
880
|
for (const env of config.environments) {
|
|
@@ -878,6 +889,7 @@ export function validateFileMap(files, options = {}) {
|
|
|
878
889
|
}
|
|
879
890
|
}
|
|
880
891
|
}
|
|
892
|
+
detectReferenceCycles(parsedConfigs, issues);
|
|
881
893
|
// Duplicate keys (case-insensitive — see detectDuplicateKeys).
|
|
882
894
|
detectDuplicateKeys(allConfigs, 'key', issues);
|
|
883
895
|
detectDuplicateKeys(allSchemaFiles, 'schema key', issues);
|
|
@@ -934,10 +946,61 @@ function validateBoundJsonValues(raw, config, schemaDocuments, file, issues) {
|
|
|
934
946
|
};
|
|
935
947
|
walk(raw, '');
|
|
936
948
|
}
|
|
949
|
+
/**
|
|
950
|
+
* Warn when a schema map (an object under `additionalProperties`) has integer
|
|
951
|
+
* keys that are not first, ascending (qfg-e87r.21). The app saves with
|
|
952
|
+
* JSON.stringify, which moves them there, so a hand-written file gets a
|
|
953
|
+
* one-time key-order diff. Same scope as validateBoundJsonValues. Always a
|
|
954
|
+
* warning: it never fails verify or rejects a push.
|
|
955
|
+
*/
|
|
956
|
+
function warnMapKeyOrder(text, raw, config, schemaDocuments, file, issues) {
|
|
957
|
+
if (config.type !== 'config' || config.valueType !== 'json' || !config.schemaKey)
|
|
958
|
+
return;
|
|
959
|
+
const schema = schemaDocuments.get(config.schemaKey);
|
|
960
|
+
if (!schema)
|
|
961
|
+
return;
|
|
962
|
+
// The file is scanned for its real key order at most once, and only if a
|
|
963
|
+
// map with an integer key exists.
|
|
964
|
+
let ordered = null;
|
|
965
|
+
const orderedDocument = () => {
|
|
966
|
+
if (ordered === null)
|
|
967
|
+
ordered = scanOrderedJson(text);
|
|
968
|
+
return ordered;
|
|
969
|
+
};
|
|
970
|
+
const walk = (node, segments, docPath) => {
|
|
971
|
+
if (Array.isArray(node)) {
|
|
972
|
+
for (const [index, child] of node.entries())
|
|
973
|
+
walk(child, [...segments, index], `${docPath}[${index}]`);
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
if (typeof node !== 'object' || node === null)
|
|
977
|
+
return;
|
|
978
|
+
const record = node;
|
|
979
|
+
if (record.type === 'json' && 'value' in record) {
|
|
980
|
+
const valueSegments = [...segments, 'value'];
|
|
981
|
+
const warnings = findMapKeyOrderWarnings(schema, record.value, () => {
|
|
982
|
+
const doc = orderedDocument();
|
|
983
|
+
return doc && navigate(doc, valueSegments);
|
|
984
|
+
}, docPath || 'value');
|
|
985
|
+
for (const warning of warnings) {
|
|
986
|
+
const keys = warning.integerKeys.map((k) => `"${k}"`).join(', ');
|
|
987
|
+
issues.push({
|
|
988
|
+
file,
|
|
989
|
+
message: `map at ${warning.path} has integer keys (${keys}) that aren't first, in ascending order. Saving from the app moves them to the front. Reorder them here to avoid a one-time diff.`,
|
|
990
|
+
severity: 'warning',
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
for (const [key, child] of Object.entries(record))
|
|
996
|
+
walk(child, [...segments, key], docPath ? `${docPath}.${key}` : key);
|
|
997
|
+
};
|
|
998
|
+
walk(raw, [], '');
|
|
999
|
+
}
|
|
937
1000
|
/**
|
|
938
1001
|
* A schema that does not compile is an error on the schema file, bound or not.
|
|
939
|
-
* Gated on `validateValues` with the value checks
|
|
940
|
-
*
|
|
1002
|
+
* Gated on `validateValues` with the value checks; the CLI and the pre-receive
|
|
1003
|
+
* hook both turn it on.
|
|
941
1004
|
*/
|
|
942
1005
|
function validateSchemaCompiles(schemaKey, schema, file, issues) {
|
|
943
1006
|
const compiled = compileSchema(schema);
|
|
@@ -1223,6 +1286,97 @@ function collectSegmentReferences(config) {
|
|
|
1223
1286
|
}
|
|
1224
1287
|
return refs;
|
|
1225
1288
|
}
|
|
1289
|
+
/**
|
|
1290
|
+
* Reject reference cycles that SDKs recurse through at eval time (qfg-9dxb.11):
|
|
1291
|
+
* - segment -> segment via IN_SEG / NOT_IN_SEG criteria;
|
|
1292
|
+
* - config -> config via `decryptWith` on a confidential value (the SDKs
|
|
1293
|
+
* resolve the key config's value to decrypt, and follow its decryptWith).
|
|
1294
|
+
* An unguarded SDK recurses until the stack overflows (sdk-net: an
|
|
1295
|
+
* uncatchable StackOverflowException), so a cycle is a hard error at write
|
|
1296
|
+
* time. Edges to keys that do not exist are skipped — a missing segment is
|
|
1297
|
+
* the reference error above. Each cycle is reported once, on the file of its
|
|
1298
|
+
* lexicographically first key, with the path starting there.
|
|
1299
|
+
*/
|
|
1300
|
+
function detectReferenceCycles(nodes, issues) {
|
|
1301
|
+
const segments = nodes.filter((n) => n.config.type === 'segment');
|
|
1302
|
+
const segmentKeys = new Set(segments.map((n) => n.config.key));
|
|
1303
|
+
reportCycles(segments.map((n) => ({
|
|
1304
|
+
key: n.config.key,
|
|
1305
|
+
relPath: n.relPath,
|
|
1306
|
+
targets: collectSegmentReferences(n.config).filter((ref) => segmentKeys.has(ref)),
|
|
1307
|
+
})), 'Segment reference cycle', 'Remove one of the IN_SEG / NOT_IN_SEG references in the cycle', issues);
|
|
1308
|
+
const configKeys = new Set(nodes.map((n) => n.config.key));
|
|
1309
|
+
reportCycles(nodes.map((n) => ({
|
|
1310
|
+
key: n.config.key,
|
|
1311
|
+
relPath: n.relPath,
|
|
1312
|
+
targets: collectDecryptWithReferences(n.raw).filter((ref) => configKeys.has(ref)),
|
|
1313
|
+
})), 'decryptWith cycle', 'A decryption key config must not itself be decrypted with a config in the cycle', issues);
|
|
1314
|
+
}
|
|
1315
|
+
function reportCycles(graph, label, suggestion, issues) {
|
|
1316
|
+
const byKey = new Map(graph.map((n) => [n.key, n]));
|
|
1317
|
+
const state = new Map();
|
|
1318
|
+
const stack = [];
|
|
1319
|
+
const seen = new Set();
|
|
1320
|
+
function visit(key) {
|
|
1321
|
+
state.set(key, 'visiting');
|
|
1322
|
+
stack.push(key);
|
|
1323
|
+
for (const target of [...new Set(byKey.get(key).targets)].sort()) {
|
|
1324
|
+
const s = state.get(target);
|
|
1325
|
+
if (s === 'visiting') {
|
|
1326
|
+
const cycle = stack.slice(stack.indexOf(target));
|
|
1327
|
+
const first = cycle.indexOf([...cycle].sort()[0]);
|
|
1328
|
+
const rotated = [...cycle.slice(first), ...cycle.slice(0, first)];
|
|
1329
|
+
const id = rotated.join('\0');
|
|
1330
|
+
if (!seen.has(id)) {
|
|
1331
|
+
seen.add(id);
|
|
1332
|
+
issues.push({
|
|
1333
|
+
file: byKey.get(rotated[0]).relPath,
|
|
1334
|
+
message: `${label}: ${[...rotated, rotated[0]].join(' -> ')}`,
|
|
1335
|
+
severity: 'error',
|
|
1336
|
+
suggestion,
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
else if (s === undefined) {
|
|
1341
|
+
visit(target);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
stack.pop();
|
|
1345
|
+
state.set(key, 'done');
|
|
1346
|
+
}
|
|
1347
|
+
for (const key of [...byKey.keys()].sort()) {
|
|
1348
|
+
if (!state.has(key))
|
|
1349
|
+
visit(key);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
/** decryptWith targets of every confidential value in a config's rules and variants. */
|
|
1353
|
+
function collectDecryptWithReferences(raw) {
|
|
1354
|
+
const refs = [];
|
|
1355
|
+
const doc = (raw ?? {});
|
|
1356
|
+
function scanValue(value) {
|
|
1357
|
+
if (!value || typeof value !== 'object')
|
|
1358
|
+
return;
|
|
1359
|
+
const v = value;
|
|
1360
|
+
if (v.type === 'weighted_values') {
|
|
1361
|
+
const wv = v.value;
|
|
1362
|
+
for (const entry of wv?.weightedValues ?? [])
|
|
1363
|
+
scanValue(entry?.value);
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
if (v.confidential === true && typeof v.decryptWith === 'string')
|
|
1367
|
+
refs.push(v.decryptWith);
|
|
1368
|
+
}
|
|
1369
|
+
function scanRules(rules) {
|
|
1370
|
+
for (const rule of rules ?? [])
|
|
1371
|
+
scanValue(rule?.value);
|
|
1372
|
+
}
|
|
1373
|
+
scanRules(doc.default?.rules);
|
|
1374
|
+
for (const env of doc.environments ?? [])
|
|
1375
|
+
scanRules(env?.rules);
|
|
1376
|
+
for (const variant of doc.variants ?? [])
|
|
1377
|
+
scanValue(variant?.value);
|
|
1378
|
+
return refs;
|
|
1379
|
+
}
|
|
1226
1380
|
function typeToDir(type) {
|
|
1227
1381
|
for (const [dir, t] of Object.entries(DIR_TO_TYPE)) {
|
|
1228
1382
|
if (t === type)
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: "0.2.
|
|
1
|
+
declare const _default: "0.2.2";
|
|
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.2.
|
|
2
|
+
export default '0.2.2';
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"packageManager": "yarn@4.11.0+sha512.4e54aeace9141df2f0177c266b05ec50dc044638157dae128c471ba65994ac802122d7ab35bcd9e81641228b7dcf24867d28e750e0bcae8a05277d600008ad54",
|
|
3
3
|
"name": "@quonfig/cli",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.2",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/quonfig/cli/issues"
|
|
7
7
|
},
|