@quonfig/cli 0.0.22 → 0.0.23
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 +36 -36
- package/dist/commands/pull.js +2 -2
- package/dist/commands/push.d.ts +6 -14
- package/dist/commands/push.js +54 -99
- package/dist/init/schema.js +4 -2
- package/dist/push/bare-path-diff.js +19 -17
- package/dist/push/config-push-client.d.ts +18 -0
- package/dist/push/config-push-client.js +82 -0
- package/dist/push/diff-summary.d.ts +11 -0
- package/dist/push/diff-summary.js +1 -8
- package/dist/push/run-push.d.ts +53 -13
- package/dist/push/run-push.js +44 -132
- package/dist/util/gitea-token-storage.js +2 -2
- package/dist/util/quonfig-home.d.ts +6 -0
- package/dist/util/quonfig-home.js +8 -0
- package/dist/util/token-storage.js +2 -2
- package/dist/verify/validate.js +2 -2
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ $ npm install -g @quonfig/cli
|
|
|
14
14
|
$ qfg COMMAND
|
|
15
15
|
running command...
|
|
16
16
|
$ qfg (--version)
|
|
17
|
-
@quonfig/cli/0.0.
|
|
17
|
+
@quonfig/cli/0.0.23 linux-x64 node-v24.4.1
|
|
18
18
|
$ qfg --help [COMMAND]
|
|
19
19
|
USAGE
|
|
20
20
|
$ qfg COMMAND
|
|
@@ -92,7 +92,7 @@ EXAMPLES
|
|
|
92
92
|
$ qfg config-schema --json-schema # full JSON Schema document (copy into your editor)
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
_See code: [src/commands/config-schema.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
95
|
+
_See code: [src/commands/config-schema.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/config-schema.ts)_
|
|
96
96
|
|
|
97
97
|
## `qfg create NAME`
|
|
98
98
|
|
|
@@ -177,7 +177,7 @@ EXAMPLES
|
|
|
177
177
|
$ qfg set-rollout my.new.flag --environment production --true-percent 20
|
|
178
178
|
```
|
|
179
179
|
|
|
180
|
-
_See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
180
|
+
_See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/create.ts)_
|
|
181
181
|
|
|
182
182
|
## `qfg delete NAME`
|
|
183
183
|
|
|
@@ -223,7 +223,7 @@ EXAMPLES
|
|
|
223
223
|
$ qfg delete my.config --yes
|
|
224
224
|
```
|
|
225
225
|
|
|
226
|
-
_See code: [src/commands/delete.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
226
|
+
_See code: [src/commands/delete.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/delete.ts)_
|
|
227
227
|
|
|
228
228
|
## `qfg flag info [NAME]`
|
|
229
229
|
|
|
@@ -426,7 +426,7 @@ EXAMPLES
|
|
|
426
426
|
$ qfg generate --targets node-ts -o ./dist # combine with targets
|
|
427
427
|
```
|
|
428
428
|
|
|
429
|
-
_See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
429
|
+
_See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/generate.ts)_
|
|
430
430
|
|
|
431
431
|
## `qfg generate-new-hex-key`
|
|
432
432
|
|
|
@@ -449,7 +449,7 @@ EXAMPLES
|
|
|
449
449
|
$ qfg generate-new-hex-key
|
|
450
450
|
```
|
|
451
451
|
|
|
452
|
-
_See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
452
|
+
_See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/generate-new-hex-key.ts)_
|
|
453
453
|
|
|
454
454
|
## `qfg get [NAME]`
|
|
455
455
|
|
|
@@ -481,7 +481,7 @@ EXAMPLES
|
|
|
481
481
|
$ qfg get my.config.name --environment=production
|
|
482
482
|
```
|
|
483
483
|
|
|
484
|
-
_See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
484
|
+
_See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/get.ts)_
|
|
485
485
|
|
|
486
486
|
## `qfg info [NAME]`
|
|
487
487
|
|
|
@@ -529,7 +529,7 @@ EXAMPLES
|
|
|
529
529
|
$ qfg info my.config.name --exclude-evaluations # skip 24h stats
|
|
530
530
|
```
|
|
531
531
|
|
|
532
|
-
_See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
532
|
+
_See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/info.ts)_
|
|
533
533
|
|
|
534
534
|
## `qfg init [DIRECTORY]`
|
|
535
535
|
|
|
@@ -569,7 +569,7 @@ EXAMPLES
|
|
|
569
569
|
$ qfg init --dry-run
|
|
570
570
|
```
|
|
571
571
|
|
|
572
|
-
_See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
572
|
+
_See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/init.ts)_
|
|
573
573
|
|
|
574
574
|
## `qfg interactive`
|
|
575
575
|
|
|
@@ -599,7 +599,7 @@ EXAMPLES
|
|
|
599
599
|
$ qfg
|
|
600
600
|
```
|
|
601
601
|
|
|
602
|
-
_See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
602
|
+
_See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/interactive.ts)_
|
|
603
603
|
|
|
604
604
|
## `qfg list`
|
|
605
605
|
|
|
@@ -639,7 +639,7 @@ EXAMPLES
|
|
|
639
639
|
$ qfg list --feature-flags
|
|
640
640
|
```
|
|
641
641
|
|
|
642
|
-
_See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
642
|
+
_See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/list.ts)_
|
|
643
643
|
|
|
644
644
|
## `qfg log-level NAME`
|
|
645
645
|
|
|
@@ -704,7 +704,7 @@ EXAMPLES
|
|
|
704
704
|
$ qfg log-level log-level.my-app --target=Chatty --value=INFO --environment=production
|
|
705
705
|
```
|
|
706
706
|
|
|
707
|
-
_See code: [src/commands/log-level.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
707
|
+
_See code: [src/commands/log-level.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/log-level.ts)_
|
|
708
708
|
|
|
709
709
|
## `qfg login`
|
|
710
710
|
|
|
@@ -729,7 +729,7 @@ EXAMPLES
|
|
|
729
729
|
$ qfg login --profile myprofile
|
|
730
730
|
```
|
|
731
731
|
|
|
732
|
-
_See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
732
|
+
_See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/login.ts)_
|
|
733
733
|
|
|
734
734
|
## `qfg logout`
|
|
735
735
|
|
|
@@ -752,7 +752,7 @@ EXAMPLES
|
|
|
752
752
|
$ qfg logout
|
|
753
753
|
```
|
|
754
754
|
|
|
755
|
-
_See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
755
|
+
_See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/logout.ts)_
|
|
756
756
|
|
|
757
757
|
## `qfg mcp`
|
|
758
758
|
|
|
@@ -785,7 +785,7 @@ EXAMPLES
|
|
|
785
785
|
$ qfg mcp --url http://local-app.quonfig-staging.com:3003/api/v1/mcp
|
|
786
786
|
```
|
|
787
787
|
|
|
788
|
-
_See code: [src/commands/mcp.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
788
|
+
_See code: [src/commands/mcp.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/mcp.ts)_
|
|
789
789
|
|
|
790
790
|
## `qfg migrate`
|
|
791
791
|
|
|
@@ -839,7 +839,7 @@ EXAMPLES
|
|
|
839
839
|
$ qfg migrate --from launch --api-key $LAUNCH_API_KEY --dir ./quonfig-config --staging
|
|
840
840
|
```
|
|
841
841
|
|
|
842
|
-
_See code: [src/commands/migrate.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
842
|
+
_See code: [src/commands/migrate.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/migrate.ts)_
|
|
843
843
|
|
|
844
844
|
## `qfg migrate doctor`
|
|
845
845
|
|
|
@@ -877,7 +877,7 @@ EXAMPLES
|
|
|
877
877
|
$ qfg migrate doctor --json
|
|
878
878
|
```
|
|
879
879
|
|
|
880
|
-
_See code: [src/commands/migrate/doctor.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
880
|
+
_See code: [src/commands/migrate/doctor.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/migrate/doctor.ts)_
|
|
881
881
|
|
|
882
882
|
## `qfg migrate my-code`
|
|
883
883
|
|
|
@@ -909,7 +909,7 @@ EXAMPLES
|
|
|
909
909
|
$ qfg migrate my-code --dry-run
|
|
910
910
|
```
|
|
911
911
|
|
|
912
|
-
_See code: [src/commands/migrate/my-code.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
912
|
+
_See code: [src/commands/migrate/my-code.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/migrate/my-code.ts)_
|
|
913
913
|
|
|
914
914
|
## `qfg migrate status`
|
|
915
915
|
|
|
@@ -940,7 +940,7 @@ EXAMPLES
|
|
|
940
940
|
$ qfg migrate status --json
|
|
941
941
|
```
|
|
942
942
|
|
|
943
|
-
_See code: [src/commands/migrate/status.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
943
|
+
_See code: [src/commands/migrate/status.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/migrate/status.ts)_
|
|
944
944
|
|
|
945
945
|
## `qfg override [NAME] [VALUE]`
|
|
946
946
|
|
|
@@ -995,7 +995,7 @@ EXAMPLES
|
|
|
995
995
|
$ qfg override my.flag true --env=staging
|
|
996
996
|
```
|
|
997
997
|
|
|
998
|
-
_See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
998
|
+
_See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/override.ts)_
|
|
999
999
|
|
|
1000
1000
|
## `qfg pull`
|
|
1001
1001
|
|
|
@@ -1039,7 +1039,7 @@ EXAMPLES
|
|
|
1039
1039
|
$ qfg pull # uses QUONFIG_DIR env var
|
|
1040
1040
|
```
|
|
1041
1041
|
|
|
1042
|
-
_See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1042
|
+
_See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/pull.ts)_
|
|
1043
1043
|
|
|
1044
1044
|
## `qfg push`
|
|
1045
1045
|
|
|
@@ -1087,7 +1087,7 @@ EXAMPLES
|
|
|
1087
1087
|
$ qfg push --dir ./our-config --yes
|
|
1088
1088
|
```
|
|
1089
1089
|
|
|
1090
|
-
_See code: [src/commands/push.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1090
|
+
_See code: [src/commands/push.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/push.ts)_
|
|
1091
1091
|
|
|
1092
1092
|
## `qfg schema NAME`
|
|
1093
1093
|
|
|
@@ -1125,7 +1125,7 @@ EXAMPLES
|
|
|
1125
1125
|
$ qfg schema my-schema --set-json-schema=@schemas/my-schema.json --protected
|
|
1126
1126
|
```
|
|
1127
1127
|
|
|
1128
|
-
_See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1128
|
+
_See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/schema.ts)_
|
|
1129
1129
|
|
|
1130
1130
|
## `qfg sdk-key`
|
|
1131
1131
|
|
|
@@ -1152,7 +1152,7 @@ EXAMPLES
|
|
|
1152
1152
|
$ qfg sdk-key revoke <key-id>
|
|
1153
1153
|
```
|
|
1154
1154
|
|
|
1155
|
-
_See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1155
|
+
_See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/sdk-key.ts)_
|
|
1156
1156
|
|
|
1157
1157
|
## `qfg sdk-key create`
|
|
1158
1158
|
|
|
@@ -1184,7 +1184,7 @@ EXAMPLES
|
|
|
1184
1184
|
$ qfg sdk-key create --environment staging --type browser
|
|
1185
1185
|
```
|
|
1186
1186
|
|
|
1187
|
-
_See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1187
|
+
_See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/sdk-key/create.ts)_
|
|
1188
1188
|
|
|
1189
1189
|
## `qfg sdk-key list`
|
|
1190
1190
|
|
|
@@ -1213,7 +1213,7 @@ EXAMPLES
|
|
|
1213
1213
|
$ qfg sdk-key list --environment production
|
|
1214
1214
|
```
|
|
1215
1215
|
|
|
1216
|
-
_See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1216
|
+
_See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/sdk-key/list.ts)_
|
|
1217
1217
|
|
|
1218
1218
|
## `qfg sdk-key revoke KEYID`
|
|
1219
1219
|
|
|
@@ -1240,7 +1240,7 @@ EXAMPLES
|
|
|
1240
1240
|
$ qfg sdk-key revoke a1b2c3d4-e5f6-7890-abcd-ef1234567890
|
|
1241
1241
|
```
|
|
1242
1242
|
|
|
1243
|
-
_See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1243
|
+
_See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/sdk-key/revoke.ts)_
|
|
1244
1244
|
|
|
1245
1245
|
## `qfg set-default [NAME]`
|
|
1246
1246
|
|
|
@@ -1311,7 +1311,7 @@ EXAMPLES
|
|
|
1311
1311
|
$ qfg pull && qfg config-schema
|
|
1312
1312
|
```
|
|
1313
1313
|
|
|
1314
|
-
_See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1314
|
+
_See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/set-default.ts)_
|
|
1315
1315
|
|
|
1316
1316
|
## `qfg set-rollout [NAME]`
|
|
1317
1317
|
|
|
@@ -1388,7 +1388,7 @@ EXAMPLES
|
|
|
1388
1388
|
$ qfg set-rollout my.variant.flag --environment production --weights "a:33,b:33,c:34" --hash-by user.id
|
|
1389
1389
|
```
|
|
1390
1390
|
|
|
1391
|
-
_See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1391
|
+
_See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/set-rollout.ts)_
|
|
1392
1392
|
|
|
1393
1393
|
## `qfg sync`
|
|
1394
1394
|
|
|
@@ -1420,7 +1420,7 @@ EXAMPLES
|
|
|
1420
1420
|
$ qfg sync --watch --dir ./our-config --interval 30
|
|
1421
1421
|
```
|
|
1422
1422
|
|
|
1423
|
-
_See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1423
|
+
_See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/sync.ts)_
|
|
1424
1424
|
|
|
1425
1425
|
## `qfg toggle [NAME]`
|
|
1426
1426
|
|
|
@@ -1522,7 +1522,7 @@ EXAMPLES
|
|
|
1522
1522
|
$ qfg verify --json
|
|
1523
1523
|
```
|
|
1524
1524
|
|
|
1525
|
-
_See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1525
|
+
_See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/verify.ts)_
|
|
1526
1526
|
|
|
1527
1527
|
## `qfg whoami`
|
|
1528
1528
|
|
|
@@ -1545,7 +1545,7 @@ EXAMPLES
|
|
|
1545
1545
|
$ qfg whoami
|
|
1546
1546
|
```
|
|
1547
1547
|
|
|
1548
|
-
_See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1548
|
+
_See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/whoami.ts)_
|
|
1549
1549
|
|
|
1550
1550
|
## `qfg workspace`
|
|
1551
1551
|
|
|
@@ -1568,7 +1568,7 @@ EXAMPLES
|
|
|
1568
1568
|
$ qfg workspace
|
|
1569
1569
|
```
|
|
1570
1570
|
|
|
1571
|
-
_See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1571
|
+
_See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/workspace.ts)_
|
|
1572
1572
|
|
|
1573
1573
|
## `qfg workspace bootstrap`
|
|
1574
1574
|
|
|
@@ -1601,7 +1601,7 @@ EXAMPLES
|
|
|
1601
1601
|
$ qfg workspace bootstrap --dir ./our-config --skip-validate
|
|
1602
1602
|
```
|
|
1603
1603
|
|
|
1604
|
-
_See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1604
|
+
_See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/workspace/bootstrap.ts)_
|
|
1605
1605
|
|
|
1606
1606
|
## `qfg workspace create SLUG`
|
|
1607
1607
|
|
|
@@ -1635,7 +1635,7 @@ EXAMPLES
|
|
|
1635
1635
|
$ qfg workspace create my-team --org 11111111-1111-1111-1111-111111111111
|
|
1636
1636
|
```
|
|
1637
1637
|
|
|
1638
|
-
_See code: [src/commands/workspace/create.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1638
|
+
_See code: [src/commands/workspace/create.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/workspace/create.ts)_
|
|
1639
1639
|
|
|
1640
1640
|
## `qfg workspace switch [SLUG]`
|
|
1641
1641
|
|
|
@@ -1663,7 +1663,7 @@ EXAMPLES
|
|
|
1663
1663
|
$ qfg workspace switch my-workspace-slug
|
|
1664
1664
|
```
|
|
1665
1665
|
|
|
1666
|
-
_See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.0.
|
|
1666
|
+
_See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.0.23/src/commands/workspace/switch.ts)_
|
|
1667
1667
|
<!-- commandsstop -->
|
|
1668
1668
|
|
|
1669
1669
|
## Local Development
|
package/dist/commands/pull.js
CHANGED
|
@@ -186,9 +186,9 @@ CLI shortcuts (no JSON editing needed for simple cases):
|
|
|
186
186
|
async maybeWriteQuonfigDir(dir) {
|
|
187
187
|
if (process.env.QUONFIG_DIR)
|
|
188
188
|
return; // already set via env
|
|
189
|
-
const os = await import('node:os');
|
|
190
189
|
const pathMod = await import('node:path');
|
|
191
|
-
const
|
|
190
|
+
const { getQuonfigConfigHome } = await import('../util/quonfig-home.js');
|
|
191
|
+
const configFilePath = pathMod.join(getQuonfigConfigHome(), 'config');
|
|
192
192
|
try {
|
|
193
193
|
const existing = await fs.promises.readFile(configFilePath, 'utf8').catch(() => '');
|
|
194
194
|
if (existing.includes('QUONFIG_DIR') || existing.includes('quonfig_dir'))
|
package/dist/commands/push.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import type { JsonObj } from '../result.js';
|
|
2
2
|
import { BaseCommand } from '../index.js';
|
|
3
3
|
import { GiteaTokenResponse } from '../util/gitea-api.js';
|
|
4
|
-
import type
|
|
5
|
-
import { runPush } from '../push/run-push.js';
|
|
4
|
+
import { runPush, type GiteaTokenMintResult } from '../push/run-push.js';
|
|
6
5
|
export default class Push extends BaseCommand {
|
|
7
6
|
static description: string;
|
|
8
7
|
static examples: string[];
|
|
@@ -34,20 +33,13 @@ export declare function giteaResponseToMintResult(resp: GiteaTokenResponse): Git
|
|
|
34
33
|
*
|
|
35
34
|
* Returns a `{deps, cleanup}` pair. The caller MUST call `cleanup()` in a
|
|
36
35
|
* finally block so the bare-path probe clone is removed even on errors.
|
|
36
|
+
*
|
|
37
|
+
* As of qfg-azk.13 the push code path no longer mints a write-scoped Gitea
|
|
38
|
+
* token; the actual commit is performed server-side by the `configs.push`
|
|
39
|
+
* oRPC procedure. We still mint a READ token to authenticate the bare-path
|
|
40
|
+
* probe-clone and the clone-path `git fetch`.
|
|
37
41
|
*/
|
|
38
42
|
export declare function buildRealDeps(cmd: Push): {
|
|
39
43
|
deps: Parameters<typeof runPush>[1];
|
|
40
44
|
cleanup: () => Promise<void>;
|
|
41
45
|
};
|
|
42
|
-
/**
|
|
43
|
-
* Copy the contents of `sourceDir` into `destDir`, skipping `.git/` and any
|
|
44
|
-
* temp push-clone scratch directories. Used as the `applyDelta` callback for
|
|
45
|
-
* bare-path pushes — we mirror local files into a fresh clone so that
|
|
46
|
-
* `cloneAndStackPush`'s `git add --all` + diff will reflect the union of
|
|
47
|
-
* (deletes, modifies, adds) we intend.
|
|
48
|
-
*
|
|
49
|
-
* IMPORTANT: this deletes files in destDir that are not in sourceDir so that
|
|
50
|
-
* deletions make it into the push. Without this the clone-and-stack produces
|
|
51
|
-
* an "adds-only" delta and destructive heuristics would never fire in the UI.
|
|
52
|
-
*/
|
|
53
|
-
export declare function copyDirMirror(sourceDir: string, destDir: string): Promise<void>;
|
package/dist/commands/push.js
CHANGED
|
@@ -5,9 +5,10 @@ import * as util from 'node:util';
|
|
|
5
5
|
import { Flags } from '@oclif/core';
|
|
6
6
|
import { BaseCommand } from '../index.js';
|
|
7
7
|
import { mintGiteaToken } from '../util/gitea-api.js';
|
|
8
|
-
import { getRemoteUrl, gitFetch, gitSetRemote, isGitRepo
|
|
9
|
-
import { PushFatalError, runPush } from '../push/run-push.js';
|
|
8
|
+
import { getRemoteUrl, gitFetch, gitSetRemote, isGitRepo } from '../util/git-ops.js';
|
|
9
|
+
import { PushFatalError, runPush, } from '../push/run-push.js';
|
|
10
10
|
import { computeBarePathDiff } from '../push/bare-path-diff.js';
|
|
11
|
+
import { callConfigsPush } from '../push/config-push-client.js';
|
|
11
12
|
import { resolveWorkspaceUuid } from '../util/resolve-workspace.js';
|
|
12
13
|
const execFile = util.promisify(execFileCb);
|
|
13
14
|
export default class Push extends BaseCommand {
|
|
@@ -127,6 +128,11 @@ export function giteaResponseToMintResult(resp) {
|
|
|
127
128
|
*
|
|
128
129
|
* Returns a `{deps, cleanup}` pair. The caller MUST call `cleanup()` in a
|
|
129
130
|
* finally block so the bare-path probe clone is removed even on errors.
|
|
131
|
+
*
|
|
132
|
+
* As of qfg-azk.13 the push code path no longer mints a write-scoped Gitea
|
|
133
|
+
* token; the actual commit is performed server-side by the `configs.push`
|
|
134
|
+
* oRPC procedure. We still mint a READ token to authenticate the bare-path
|
|
135
|
+
* probe-clone and the clone-path `git fetch`.
|
|
130
136
|
*/
|
|
131
137
|
export function buildRealDeps(cmd) {
|
|
132
138
|
const log = (line) => cmd.log(line);
|
|
@@ -187,12 +193,6 @@ export function buildRealDeps(cmd) {
|
|
|
187
193
|
const probe = await ensureBarePathProbe(dir);
|
|
188
194
|
return probe.deltas;
|
|
189
195
|
},
|
|
190
|
-
async push(dir) {
|
|
191
|
-
await execFile('git', ['-C', dir, 'push', 'origin', 'main']);
|
|
192
|
-
},
|
|
193
|
-
async getLocalAuthor(dir) {
|
|
194
|
-
return readLocalAuthor(dir);
|
|
195
|
-
},
|
|
196
196
|
async countFilesInRemote(dir) {
|
|
197
197
|
if (await isGitRepo(dir)) {
|
|
198
198
|
const fromGit = await countTrackedFilesAtRef(dir, 'origin/main');
|
|
@@ -207,9 +207,11 @@ export function buildRealDeps(cmd) {
|
|
|
207
207
|
};
|
|
208
208
|
const deps = {
|
|
209
209
|
async mintWriteToken(requestedTarget) {
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
//
|
|
210
|
+
// Read scope: this token authenticates probe-clone and clone-path
|
|
211
|
+
// fetch. The actual commit goes through the server via configs.push,
|
|
212
|
+
// which is authorized off the user's WorkOS session — no write PAT
|
|
213
|
+
// needed in the push code path (qfg-azk.13).
|
|
214
|
+
const resp = await mintGiteaToken(requestedTarget, 'read', 'pull');
|
|
213
215
|
authenticatedRepoUrl = resp.repoUrl;
|
|
214
216
|
return giteaResponseToMintResult(resp);
|
|
215
217
|
},
|
|
@@ -220,7 +222,7 @@ export function buildRealDeps(cmd) {
|
|
|
220
222
|
return { errors };
|
|
221
223
|
},
|
|
222
224
|
gitOps,
|
|
223
|
-
|
|
225
|
+
pushToServer: callConfigsPush,
|
|
224
226
|
log,
|
|
225
227
|
errLog,
|
|
226
228
|
};
|
|
@@ -237,26 +239,20 @@ export function buildRealDeps(cmd) {
|
|
|
237
239
|
return { deps, cleanup };
|
|
238
240
|
}
|
|
239
241
|
/**
|
|
240
|
-
* Diff the working tree (what we are about to push) against origin/main
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
242
|
+
* Diff the working tree (what we are about to push) against origin/main and
|
|
243
|
+
* fill in before/after JSON content for each delta as required by the
|
|
244
|
+
* `configs.push` wire shape (qfg-azk.13).
|
|
245
|
+
*
|
|
246
|
+
* Added — afterJson = working tree
|
|
247
|
+
* Modified — beforeJson = `git show origin/main:<path>`, afterJson = working tree
|
|
248
|
+
* Deleted — beforeJson = `git show origin/main:<path>`
|
|
245
249
|
*
|
|
246
250
|
* `git diff A..B` reports B relative to A. So `HEAD..origin/main` tells us
|
|
247
251
|
* what origin has that we don't. We want "local vs remote" with local as the
|
|
248
252
|
* baseline — i.e. origin/main..HEAD.
|
|
249
253
|
*/
|
|
250
254
|
async function diffHeadVsOrigin(dir) {
|
|
251
|
-
|
|
252
|
-
// 'A' -> added locally, 'D' -> deleted locally, 'M' -> modified locally.
|
|
253
|
-
const { stdout } = await execFile('git', [
|
|
254
|
-
'-C',
|
|
255
|
-
dir,
|
|
256
|
-
'diff',
|
|
257
|
-
'--name-status',
|
|
258
|
-
'origin/main..HEAD',
|
|
259
|
-
]);
|
|
255
|
+
const { stdout } = await execFile('git', ['-C', dir, 'diff', '--name-status', 'origin/main..HEAD']);
|
|
260
256
|
const deltas = [];
|
|
261
257
|
for (const raw of stdout.split('\n')) {
|
|
262
258
|
const line = raw.trim();
|
|
@@ -266,95 +262,54 @@ async function diffHeadVsOrigin(dir) {
|
|
|
266
262
|
const pathStr = rest.join(' ');
|
|
267
263
|
if (!pathStr)
|
|
268
264
|
continue;
|
|
269
|
-
if (status.startsWith('A'))
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
deltas.push({ kind: '
|
|
265
|
+
if (status.startsWith('A')) {
|
|
266
|
+
// eslint-disable-next-line no-await-in-loop
|
|
267
|
+
const afterJson = await readWorkingTreeFile(dir, pathStr);
|
|
268
|
+
deltas.push({ kind: 'added', path: pathStr, ...(afterJson === undefined ? {} : { afterJson }) });
|
|
269
|
+
}
|
|
270
|
+
else if (status.startsWith('D')) {
|
|
271
|
+
// eslint-disable-next-line no-await-in-loop
|
|
272
|
+
const beforeJson = await showAtRef(dir, 'origin/main', pathStr);
|
|
273
|
+
deltas.push({ kind: 'deleted', path: pathStr, ...(beforeJson === undefined ? {} : { beforeJson }) });
|
|
274
|
+
}
|
|
273
275
|
else if (status.startsWith('M') || status.startsWith('R') || status.startsWith('C')) {
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
+
// eslint-disable-next-line no-await-in-loop
|
|
277
|
+
const beforeJson = await showAtRef(dir, 'origin/main', pathStr);
|
|
278
|
+
// eslint-disable-next-line no-await-in-loop
|
|
279
|
+
const afterJson = await readWorkingTreeFile(dir, pathStr);
|
|
280
|
+
deltas.push({
|
|
281
|
+
kind: 'modified',
|
|
282
|
+
path: pathStr,
|
|
283
|
+
...(beforeJson === undefined ? {} : { beforeJson }),
|
|
284
|
+
...(afterJson === undefined ? {} : { afterJson }),
|
|
285
|
+
});
|
|
276
286
|
}
|
|
277
287
|
}
|
|
278
288
|
return deltas;
|
|
279
289
|
}
|
|
280
|
-
async function
|
|
290
|
+
async function showAtRef(dir, ref, relPath) {
|
|
281
291
|
try {
|
|
282
|
-
const { stdout } = await execFile('git', ['-C', dir, '
|
|
283
|
-
return stdout
|
|
292
|
+
const { stdout } = await execFile('git', ['-C', dir, 'show', `${ref}:${relPath}`]);
|
|
293
|
+
return stdout;
|
|
284
294
|
}
|
|
285
295
|
catch {
|
|
286
|
-
return
|
|
296
|
+
return undefined;
|
|
287
297
|
}
|
|
288
298
|
}
|
|
289
|
-
async function
|
|
299
|
+
async function readWorkingTreeFile(dir, relPath) {
|
|
290
300
|
try {
|
|
291
|
-
|
|
292
|
-
const { stdout: email } = await execFile('git', ['-C', dir, 'config', 'user.email']);
|
|
293
|
-
const n = name.trim();
|
|
294
|
-
const e = email.trim();
|
|
295
|
-
if (!n || !e)
|
|
296
|
-
return undefined;
|
|
297
|
-
return { name: n, email: e };
|
|
301
|
+
return await fs.promises.readFile(path.join(dir, relPath), 'utf8');
|
|
298
302
|
}
|
|
299
303
|
catch {
|
|
300
304
|
return undefined;
|
|
301
305
|
}
|
|
302
306
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
* `cloneAndStackPush`'s `git add --all` + diff will reflect the union of
|
|
308
|
-
* (deletes, modifies, adds) we intend.
|
|
309
|
-
*
|
|
310
|
-
* IMPORTANT: this deletes files in destDir that are not in sourceDir so that
|
|
311
|
-
* deletions make it into the push. Without this the clone-and-stack produces
|
|
312
|
-
* an "adds-only" delta and destructive heuristics would never fire in the UI.
|
|
313
|
-
*/
|
|
314
|
-
export async function copyDirMirror(sourceDir, destDir) {
|
|
315
|
-
// Collect source entries (except .git, .quonfig-push-clone-*)
|
|
316
|
-
const sourceRel = collectRelPaths(sourceDir);
|
|
317
|
-
const destRel = collectRelPaths(destDir);
|
|
318
|
-
// Delete anything in dest that isn't in source
|
|
319
|
-
for (const rel of destRel) {
|
|
320
|
-
if (!sourceRel.has(rel)) {
|
|
321
|
-
const full = path.join(destDir, rel);
|
|
322
|
-
try {
|
|
323
|
-
fs.rmSync(full, { force: true });
|
|
324
|
-
}
|
|
325
|
-
catch {
|
|
326
|
-
/* ignore */
|
|
327
|
-
}
|
|
328
|
-
}
|
|
307
|
+
async function countTrackedFilesAtRef(dir, ref) {
|
|
308
|
+
try {
|
|
309
|
+
const { stdout } = await execFile('git', ['-C', dir, 'ls-tree', '-r', '--name-only', ref]);
|
|
310
|
+
return stdout.split('\n').filter((l) => l.trim().length > 0).length;
|
|
329
311
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const src = path.join(sourceDir, rel);
|
|
333
|
-
const dst = path.join(destDir, rel);
|
|
334
|
-
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
335
|
-
fs.copyFileSync(src, dst);
|
|
312
|
+
catch {
|
|
313
|
+
return 0;
|
|
336
314
|
}
|
|
337
315
|
}
|
|
338
|
-
function collectRelPaths(root) {
|
|
339
|
-
const out = new Set();
|
|
340
|
-
if (!fs.existsSync(root))
|
|
341
|
-
return out;
|
|
342
|
-
const walk = (dir, prefix) => {
|
|
343
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
344
|
-
if (entry.name === '.git')
|
|
345
|
-
continue;
|
|
346
|
-
if (entry.name.startsWith('.quonfig-push-clone-'))
|
|
347
|
-
continue;
|
|
348
|
-
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
349
|
-
const full = path.join(dir, entry.name);
|
|
350
|
-
if (entry.isDirectory()) {
|
|
351
|
-
walk(full, rel);
|
|
352
|
-
}
|
|
353
|
-
else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
354
|
-
out.add(rel);
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
};
|
|
358
|
-
walk(root, '');
|
|
359
|
-
return out;
|
|
360
|
-
}
|
package/dist/init/schema.js
CHANGED
|
@@ -298,8 +298,10 @@ export function storedConfigJsonSchema() {
|
|
|
298
298
|
type: 'string',
|
|
299
299
|
description: 'References a schema in schemas/ or schemas-protected/. The referenced file must exist.',
|
|
300
300
|
},
|
|
301
|
-
|
|
302
|
-
|
|
301
|
+
access: {
|
|
302
|
+
enum: ['support', 'standard', 'protected-env', 'protected-all-envs'],
|
|
303
|
+
description: 'Edit-access tier for this config. Defaults to "standard" when absent. See protecting-access.md.',
|
|
304
|
+
},
|
|
303
305
|
tags: {
|
|
304
306
|
type: 'array',
|
|
305
307
|
items: { type: 'string' },
|
|
@@ -54,35 +54,37 @@ export async function computeBarePathDiff(localDir, remoteUrl) {
|
|
|
54
54
|
}
|
|
55
55
|
throw error;
|
|
56
56
|
}
|
|
57
|
-
const localFiles =
|
|
58
|
-
const remoteFiles =
|
|
57
|
+
const localFiles = collectFiles(localDir);
|
|
58
|
+
const remoteFiles = collectFiles(scratchDir);
|
|
59
59
|
const deltas = [];
|
|
60
|
-
for (const [rel,
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
deltas.push({ kind: 'added', path: rel });
|
|
60
|
+
for (const [rel, local] of localFiles) {
|
|
61
|
+
const remote = remoteFiles.get(rel);
|
|
62
|
+
if (remote === undefined) {
|
|
63
|
+
deltas.push({ kind: 'added', path: rel, afterJson: local.content });
|
|
64
64
|
}
|
|
65
|
-
else if (
|
|
66
|
-
deltas.push({ kind: 'modified', path: rel });
|
|
65
|
+
else if (remote.hash !== local.hash) {
|
|
66
|
+
deltas.push({ kind: 'modified', path: rel, beforeJson: remote.content, afterJson: local.content });
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
-
for (const rel of remoteFiles
|
|
69
|
+
for (const [rel, remote] of remoteFiles) {
|
|
70
70
|
if (!localFiles.has(rel)) {
|
|
71
|
-
deltas.push({ kind: 'deleted', path: rel });
|
|
71
|
+
deltas.push({ kind: 'deleted', path: rel, beforeJson: remote.content });
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
return { deltas, scratchDir, totalRemoteFiles: remoteFiles.size };
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
77
|
-
* Walk `root` and return a map of relative-path -> content
|
|
78
|
-
* non-dotfile file under it.
|
|
77
|
+
* Walk `root` and return a map of relative-path -> {content, hash} for every
|
|
78
|
+
* non-dotfile file under it. The content string is needed by qfg-azk.13's
|
|
79
|
+
* `configs.push` wire shape; the hash is used to short-circuit the
|
|
80
|
+
* unchanged-file case without comparing full bodies.
|
|
79
81
|
*
|
|
80
82
|
* We skip any entry whose name starts with a `.`, which covers `.git/`,
|
|
81
83
|
* `.DS_Store`, and the transient `.quonfig-push-clone-*` scratch dirs that
|
|
82
84
|
* `run-push.ts` creates next to the local workspace. Keeping the rule simple
|
|
83
85
|
* and symmetric on both sides prevents one-off false positives.
|
|
84
86
|
*/
|
|
85
|
-
function
|
|
87
|
+
function collectFiles(root) {
|
|
86
88
|
const out = new Map();
|
|
87
89
|
if (!fs.existsSync(root))
|
|
88
90
|
return out;
|
|
@@ -104,8 +106,8 @@ function collectFilesWithHash(root) {
|
|
|
104
106
|
}
|
|
105
107
|
else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
106
108
|
try {
|
|
107
|
-
const
|
|
108
|
-
out.set(rel,
|
|
109
|
+
const content = fs.readFileSync(full, 'utf8');
|
|
110
|
+
out.set(rel, { content, hash: hashString(content) });
|
|
109
111
|
}
|
|
110
112
|
catch {
|
|
111
113
|
/* ignore unreadable files — they won't appear on either side */
|
|
@@ -121,6 +123,6 @@ function collectFilesWithHash(root) {
|
|
|
121
123
|
* fingerprint so we can say "file A on local matches file A on remote". The
|
|
122
124
|
* contents are small JSON in practice, so sha1 is plenty fast.
|
|
123
125
|
*/
|
|
124
|
-
function
|
|
125
|
-
return createHash('sha1').update(
|
|
126
|
+
function hashString(s) {
|
|
127
|
+
return createHash('sha1').update(s).digest('hex');
|
|
126
128
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the `configs.push` oRPC procedure (qfg-azk.13).
|
|
3
|
+
*
|
|
4
|
+
* Server-side spec lives at
|
|
5
|
+
* `app-quonfig/src/lib/orpc/routes/config-push-handler.ts`. We translate the
|
|
6
|
+
* oRPC error shape into a tagged union the CLI can pattern-match on:
|
|
7
|
+
*
|
|
8
|
+
* - 200 → `{kind: 'success', commitSha}`
|
|
9
|
+
* - 403 with `data.denials` → `{kind: 'denied', denials}`
|
|
10
|
+
* - 409 → `{kind: 'conflict', message}`
|
|
11
|
+
* - 400 → `{kind: 'bad-request', message}`
|
|
12
|
+
* - anything else → throws so runPush surfaces a generic PushFatalError.
|
|
13
|
+
*
|
|
14
|
+
* The auth header is a WorkOS access token (or a `qf_uk_*` API key when
|
|
15
|
+
* `QUONFIG_API_KEY` is set in the environment); see `getValidAccessToken`.
|
|
16
|
+
*/
|
|
17
|
+
import type { ConfigPushInput, ConfigPushResult } from './run-push.js';
|
|
18
|
+
export declare const callConfigsPush: (input: ConfigPushInput) => Promise<ConfigPushResult>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the `configs.push` oRPC procedure (qfg-azk.13).
|
|
3
|
+
*
|
|
4
|
+
* Server-side spec lives at
|
|
5
|
+
* `app-quonfig/src/lib/orpc/routes/config-push-handler.ts`. We translate the
|
|
6
|
+
* oRPC error shape into a tagged union the CLI can pattern-match on:
|
|
7
|
+
*
|
|
8
|
+
* - 200 → `{kind: 'success', commitSha}`
|
|
9
|
+
* - 403 with `data.denials` → `{kind: 'denied', denials}`
|
|
10
|
+
* - 409 → `{kind: 'conflict', message}`
|
|
11
|
+
* - 400 → `{kind: 'bad-request', message}`
|
|
12
|
+
* - anything else → throws so runPush surfaces a generic PushFatalError.
|
|
13
|
+
*
|
|
14
|
+
* The auth header is a WorkOS access token (or a `qf_uk_*` API key when
|
|
15
|
+
* `QUONFIG_API_KEY` is set in the environment); see `getValidAccessToken`.
|
|
16
|
+
*/
|
|
17
|
+
import { getApiUrl } from '../util/domain-urls.js';
|
|
18
|
+
import { getValidAccessToken } from '../util/get-valid-token.js';
|
|
19
|
+
export const callConfigsPush = async (input) => {
|
|
20
|
+
const accessToken = await getValidAccessToken();
|
|
21
|
+
const apiUrl = getApiUrl();
|
|
22
|
+
const res = await fetch(`${apiUrl}/api/v1/configs/push`, {
|
|
23
|
+
method: 'POST',
|
|
24
|
+
headers: {
|
|
25
|
+
Authorization: `Bearer ${accessToken}`,
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
},
|
|
28
|
+
body: JSON.stringify({ json: input }),
|
|
29
|
+
});
|
|
30
|
+
if (res.ok) {
|
|
31
|
+
const body = (await res.json());
|
|
32
|
+
const commitSha = body.json?.commitSha;
|
|
33
|
+
if (!commitSha) {
|
|
34
|
+
throw new Error(`configs.push returned 200 but no commitSha (body=${JSON.stringify(body)})`);
|
|
35
|
+
}
|
|
36
|
+
return { kind: 'success', commitSha };
|
|
37
|
+
}
|
|
38
|
+
const text = await res.text().catch(() => '');
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
const body = JSON.parse(text);
|
|
42
|
+
parsed = body.json ?? body;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* keep parsed undefined */
|
|
46
|
+
}
|
|
47
|
+
const message = parsed?.message ?? text ?? `HTTP ${res.status}`;
|
|
48
|
+
if (res.status === 403) {
|
|
49
|
+
const denials = extractDenials(parsed?.data);
|
|
50
|
+
if (denials.length > 0) {
|
|
51
|
+
return { kind: 'denied', denials };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (res.status === 409) {
|
|
55
|
+
return { kind: 'conflict', message };
|
|
56
|
+
}
|
|
57
|
+
if (res.status === 400) {
|
|
58
|
+
return { kind: 'bad-request', message };
|
|
59
|
+
}
|
|
60
|
+
throw new Error(`configs.push failed (HTTP ${res.status}): ${message}`);
|
|
61
|
+
};
|
|
62
|
+
function extractDenials(data) {
|
|
63
|
+
if (!data || typeof data !== 'object')
|
|
64
|
+
return [];
|
|
65
|
+
const denials = data.denials;
|
|
66
|
+
if (!Array.isArray(denials))
|
|
67
|
+
return [];
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const raw of denials) {
|
|
70
|
+
if (!raw || typeof raw !== 'object')
|
|
71
|
+
continue;
|
|
72
|
+
const r = raw;
|
|
73
|
+
if (typeof r.path !== 'string' || typeof r.requiredPermission !== 'string')
|
|
74
|
+
continue;
|
|
75
|
+
out.push({
|
|
76
|
+
path: r.path,
|
|
77
|
+
reason: typeof r.reason === 'string' ? r.reason : `Missing required permission: ${r.requiredPermission}`,
|
|
78
|
+
requiredPermission: r.requiredPermission,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
@@ -14,6 +14,17 @@
|
|
|
14
14
|
*/
|
|
15
15
|
/** A single file-level change between the local working tree and remote HEAD. */
|
|
16
16
|
export interface FileDelta {
|
|
17
|
+
/**
|
|
18
|
+
* Local content for `added`/`modified`. Absent for `deleted`. Required by
|
|
19
|
+
* the `configs.push` server endpoint when sending the delta over the wire.
|
|
20
|
+
*/
|
|
21
|
+
afterJson?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Remote content for `modified`/`deleted`. Absent for `added`. Required by
|
|
24
|
+
* the `configs.push` server endpoint to deep-diff per-environment rule
|
|
25
|
+
* changes for authorization.
|
|
26
|
+
*/
|
|
27
|
+
beforeJson?: string;
|
|
17
28
|
/**
|
|
18
29
|
* `added` — present locally, absent on remote
|
|
19
30
|
* `modified` — present in both, content differs
|
|
@@ -16,14 +16,7 @@
|
|
|
16
16
|
* Top-level directories we present as named rows. Anything else funnels into
|
|
17
17
|
* `other`. Keeping this closed-set avoids one-off typos silently looking fine.
|
|
18
18
|
*/
|
|
19
|
-
const KNOWN_GROUPS = [
|
|
20
|
-
'configs',
|
|
21
|
-
'feature-flags',
|
|
22
|
-
'segments',
|
|
23
|
-
'schemas',
|
|
24
|
-
'schemas-protected',
|
|
25
|
-
'log-levels',
|
|
26
|
-
];
|
|
19
|
+
const KNOWN_GROUPS = ['configs', 'feature-flags', 'segments', 'schemas', 'schemas-protected', 'log-levels'];
|
|
27
20
|
const DESTRUCTIVE_DELETE_COUNT = 10;
|
|
28
21
|
const DESTRUCTIVE_DELETE_RATIO = 0.25;
|
|
29
22
|
function emptyCounts() {
|
package/dist/push/run-push.d.ts
CHANGED
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
* `--yes` skips the normal Y/N confirmation. It NEVER skips a typed-slug
|
|
19
19
|
* confirmation — that prompt is the destructive-change brake.
|
|
20
20
|
*/
|
|
21
|
-
import { PushIdentity } from '../util/clone-and-stack-push.js';
|
|
22
21
|
import { FileDelta } from './diff-summary.js';
|
|
23
22
|
export interface GiteaTokenMintResult {
|
|
24
23
|
expiresAt: string | null;
|
|
@@ -32,23 +31,52 @@ export interface GitOps {
|
|
|
32
31
|
countFilesInRemote(dir: string): Promise<number>;
|
|
33
32
|
/**
|
|
34
33
|
* Produce the list of file deltas to render in the Guard 3 summary and
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* send to the server. Each delta carries before/after JSON content per
|
|
35
|
+
* the `configs.push` wire shape (qfg-azk.13). For the clone path this is
|
|
36
|
+
* HEAD vs origin/main; for the bare path it's local vs probe-clone.
|
|
37
37
|
*/
|
|
38
38
|
diffHeadVsOrigin(dir: string): Promise<FileDelta[]>;
|
|
39
39
|
/** `git fetch origin` in the given dir. */
|
|
40
40
|
fetch(dir: string): Promise<void>;
|
|
41
|
-
/** Read local git config for author identity (used for bare-path commits). */
|
|
42
|
-
getLocalAuthor(dir: string): Promise<PushIdentity | undefined>;
|
|
43
41
|
/** Returns the `remote.origin.url` for the repo, or undefined if unset / not a repo. */
|
|
44
42
|
getRemoteOriginUrl(dir: string): Promise<string | undefined>;
|
|
45
43
|
/** Returns true if the dir has a `.git/` (worktree or repo). */
|
|
46
44
|
isGitRepo(dir: string): Promise<boolean>;
|
|
47
|
-
/** `git push origin main`. Throws with a PushConflict-ish message on non-ff. */
|
|
48
|
-
push(dir: string): Promise<void>;
|
|
49
45
|
/** Set origin to `url` (add if missing, set-url if present). */
|
|
50
46
|
setRemoteOrigin(dir: string, url: string): Promise<void>;
|
|
51
47
|
}
|
|
48
|
+
/** Server-side `kind` enum (matches `FileDeltaSchema` in app-quonfig). */
|
|
49
|
+
export type ServerFileKind = 'add' | 'delete' | 'modify';
|
|
50
|
+
export interface ServerFileDelta {
|
|
51
|
+
afterJson?: string;
|
|
52
|
+
beforeJson?: string;
|
|
53
|
+
kind: ServerFileKind;
|
|
54
|
+
path: string;
|
|
55
|
+
}
|
|
56
|
+
export interface ConfigPushInput {
|
|
57
|
+
expectedSha?: string;
|
|
58
|
+
files: ServerFileDelta[];
|
|
59
|
+
message?: string;
|
|
60
|
+
workspaceId: string;
|
|
61
|
+
}
|
|
62
|
+
export type ConfigPushResult = {
|
|
63
|
+
kind: 'bad-request';
|
|
64
|
+
message: string;
|
|
65
|
+
} | {
|
|
66
|
+
kind: 'conflict';
|
|
67
|
+
message: string;
|
|
68
|
+
} | {
|
|
69
|
+
commitSha: string;
|
|
70
|
+
kind: 'success';
|
|
71
|
+
} | {
|
|
72
|
+
denials: PushDenial[];
|
|
73
|
+
kind: 'denied';
|
|
74
|
+
};
|
|
75
|
+
export interface PushDenial {
|
|
76
|
+
path: string;
|
|
77
|
+
reason: string;
|
|
78
|
+
requiredPermission: string;
|
|
79
|
+
}
|
|
52
80
|
export type ConfirmIO = {
|
|
53
81
|
input?: NodeJS.ReadableStream;
|
|
54
82
|
output?: NodeJS.WritableStream;
|
|
@@ -70,18 +98,30 @@ export interface RunPushInput {
|
|
|
70
98
|
export interface RunPushDeps {
|
|
71
99
|
/** Optional io streams for confirmation prompts. Defaults to process stdin/stdout. */
|
|
72
100
|
confirmIO?: ConfirmIO;
|
|
73
|
-
/**
|
|
74
|
-
* Copy files from `sourceDir` into `destDir`, skipping `.git/`. Used by the
|
|
75
|
-
* bare path as the `applyDelta` callback handed to `cloneAndStackPush`.
|
|
76
|
-
*/
|
|
77
|
-
copyDirMirror(sourceDir: string, destDir: string): Promise<void>;
|
|
78
101
|
/** Error logger; defaults to console.error. */
|
|
79
102
|
errLog?: (line: string) => void;
|
|
80
103
|
gitOps: GitOps;
|
|
81
104
|
/** Logger; defaults to console.log. */
|
|
82
105
|
log?: (line: string) => void;
|
|
83
|
-
/**
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the backend's identity for this workspace. Returns repoUrl,
|
|
108
|
+
* workspaceSlug, workspaceId — used by the identity check and (for
|
|
109
|
+
* read-only auth) by the bare-path probe-clone and clone-path fetch.
|
|
110
|
+
*
|
|
111
|
+
* Named `mintWriteToken` for historical reasons; as of qfg-azk.13 the
|
|
112
|
+
* push code path no longer mints a write-scoped Gitea token (server-side
|
|
113
|
+
* commit via `configs.push`), so the real implementation now mints a
|
|
114
|
+
* read-scope token. The name is preserved to keep the test harness shape
|
|
115
|
+
* stable across the qfg-azk.13 transition.
|
|
116
|
+
*/
|
|
84
117
|
mintWriteToken(requestedTarget: string): Promise<GiteaTokenMintResult>;
|
|
118
|
+
/**
|
|
119
|
+
* Call the server-side `configs.push` oRPC procedure. Returns a tagged
|
|
120
|
+
* union so callers can distinguish success / per-file denials / a stale
|
|
121
|
+
* expectedSha conflict / a path allow-list violation without parsing
|
|
122
|
+
* HTTP status codes.
|
|
123
|
+
*/
|
|
124
|
+
pushToServer(input: ConfigPushInput): Promise<ConfigPushResult>;
|
|
85
125
|
/** Run `qfg validate` semantics. Should throw on error. */
|
|
86
126
|
validate(dir: string): Promise<{
|
|
87
127
|
errors: string[];
|
package/dist/push/run-push.js
CHANGED
|
@@ -19,8 +19,6 @@
|
|
|
19
19
|
* confirmation — that prompt is the destructive-change brake.
|
|
20
20
|
*/
|
|
21
21
|
import * as fs from 'node:fs';
|
|
22
|
-
import * as path from 'node:path';
|
|
23
|
-
import { MIGRATOR_IDENTITY, cloneAndStackPush } from '../util/clone-and-stack-push.js';
|
|
24
22
|
import { readWorkspaceSlug, writeWorkspaceSlug } from '../util/quonfig-json.js';
|
|
25
23
|
import { checkIdentity } from './identity-check.js';
|
|
26
24
|
import { confirmTypedSlug, confirmYesNo } from './confirm.js';
|
|
@@ -118,19 +116,18 @@ export async function runPush(input, deps) {
|
|
|
118
116
|
// repo into a scratch dir inside `.quonfig-push-clone/` and stack a commit
|
|
119
117
|
// that mirrors the local dir's contents on top.
|
|
120
118
|
const isClonePath = hasGit && remoteOriginUrl !== undefined && sameRepo(remoteOriginUrl, backend.repoUrl);
|
|
119
|
+
const dispatchedAs = isClonePath ? 'clone-path' : 'bare-path';
|
|
120
|
+
// Clone path: align origin with the backend so the gitOps.fetch + diff
|
|
121
|
+
// walk authenticates against the read URL we minted. The bare path's
|
|
122
|
+
// probe-clone path bypasses local origin entirely.
|
|
121
123
|
if (isClonePath) {
|
|
122
|
-
|
|
124
|
+
await deps.gitOps.setRemoteOrigin(input.dir, backend.repoUrl);
|
|
125
|
+
log('Fetching from remote...');
|
|
126
|
+
await deps.gitOps.fetch(input.dir);
|
|
123
127
|
}
|
|
124
|
-
return doBarePath(input, deps, backend, requiresTypedSlug, repoPinSlug === undefined, log);
|
|
125
|
-
}
|
|
126
|
-
async function doClonePath(input, deps, backend, requiresTypedSlug, unpinned, log) {
|
|
127
|
-
// Point origin at the authenticated URL so `git fetch` / `git push` work
|
|
128
|
-
// without touching global credential helpers.
|
|
129
|
-
await deps.gitOps.setRemoteOrigin(input.dir, backend.repoUrl);
|
|
130
|
-
log('Fetching from remote...');
|
|
131
|
-
await deps.gitOps.fetch(input.dir);
|
|
132
128
|
const deltas = await deps.gitOps.diffHeadVsOrigin(input.dir);
|
|
133
129
|
const totalFilesInRemote = await deps.gitOps.countFilesInRemote(input.dir);
|
|
130
|
+
const unpinned = repoPinSlug === undefined;
|
|
134
131
|
const summary = summarizeDiff(deltas, { totalFilesInRemote, unpinned });
|
|
135
132
|
log('');
|
|
136
133
|
log(summary.renderText({
|
|
@@ -140,7 +137,6 @@ async function doClonePath(input, deps, backend, requiresTypedSlug, unpinned, lo
|
|
|
140
137
|
localDir: input.dir,
|
|
141
138
|
}));
|
|
142
139
|
log('');
|
|
143
|
-
// Short-circuit: literally nothing to push.
|
|
144
140
|
if (summary.totals.filesTouched === 0) {
|
|
145
141
|
return { kind: 'no-op', reason: 'Local tree matches remote HEAD. Nothing to push.' };
|
|
146
142
|
}
|
|
@@ -155,141 +151,57 @@ async function doClonePath(input, deps, backend, requiresTypedSlug, unpinned, lo
|
|
|
155
151
|
log('Aborted, nothing pushed.');
|
|
156
152
|
return { kind: 'aborted', reason: 'user declined at confirm prompt' };
|
|
157
153
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
154
|
+
const baseMessage = input.message ?? `qfg push: ${summary.totals.filesTouched} file change(s)`;
|
|
155
|
+
const commitMessage = withPushedViaTrailer(baseMessage);
|
|
156
|
+
const serverFiles = deltas.map((d) => ({
|
|
157
|
+
path: d.path,
|
|
158
|
+
kind: toServerKind(d.kind),
|
|
159
|
+
...(d.beforeJson === undefined ? {} : { beforeJson: d.beforeJson }),
|
|
160
|
+
...(d.afterJson === undefined ? {} : { afterJson: d.afterJson }),
|
|
161
|
+
}));
|
|
162
|
+
log('Sending push to Quonfig cloud...');
|
|
163
|
+
const result = await deps.pushToServer({
|
|
164
|
+
workspaceId: backend.workspaceId,
|
|
165
|
+
files: serverFiles,
|
|
166
|
+
message: commitMessage,
|
|
167
|
+
});
|
|
168
|
+
if (result.kind === 'denied') {
|
|
169
|
+
errLog(`Push denied for ${result.denials.length} file(s):`);
|
|
170
|
+
for (const d of result.denials) {
|
|
171
|
+
errLog(` ${d.path}: missing permission ${d.requiredPermission}`);
|
|
166
172
|
}
|
|
167
|
-
throw new PushFatalError(`Push
|
|
173
|
+
throw new PushFatalError(`Push denied for ${result.denials.length} file(s). See errors above.`, 'PUSH_DENIED');
|
|
174
|
+
}
|
|
175
|
+
if (result.kind === 'conflict') {
|
|
176
|
+
throw new PushFatalError(`Remote moved while preparing this push (${result.message}). Run \`qfg pull\` and retry.`, 'CONFLICT');
|
|
177
|
+
}
|
|
178
|
+
if (result.kind === 'bad-request') {
|
|
179
|
+
throw new PushFatalError(`Push rejected by server: ${result.message}`, 'BAD_REQUEST');
|
|
168
180
|
}
|
|
169
|
-
// Offer to pin the workspace slug in quonfig.json if missing. Non-fatal.
|
|
170
181
|
if (unpinned && !input.noPinWrite) {
|
|
171
182
|
try {
|
|
172
183
|
await writeWorkspaceSlug(input.dir, backend.workspaceSlug);
|
|
173
184
|
log(`Wrote workspace = "${backend.workspaceSlug}" into quonfig.json`);
|
|
174
185
|
}
|
|
175
186
|
catch {
|
|
176
|
-
|
|
187
|
+
/* non-fatal; pin is advisory */
|
|
177
188
|
}
|
|
178
189
|
}
|
|
179
|
-
return { kind: 'pushed', dispatchedAs:
|
|
190
|
+
return { kind: 'pushed', dispatchedAs, commitSha: result.commitSha };
|
|
180
191
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// Local author for the migrated commit. Prefer the local git config in
|
|
186
|
-
// `input.dir` so attribution matches what the user has set. If it is not
|
|
187
|
-
// available, fall back to the migrator identity but tell the user — the plan
|
|
188
|
-
// requires this be loud.
|
|
189
|
-
let author = MIGRATOR_IDENTITY;
|
|
190
|
-
try {
|
|
191
|
-
const local = await deps.gitOps.getLocalAuthor(input.dir);
|
|
192
|
-
if (local && local.name && local.email) {
|
|
193
|
-
author = local;
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
log(`Warning: no local git user.name/user.email in ${input.dir}; commits will be attributed to ${MIGRATOR_IDENTITY.name} <${MIGRATOR_IDENTITY.email}>.`);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
catch {
|
|
200
|
-
log(`Warning: could not read local git user identity; commits will be attributed to ${MIGRATOR_IDENTITY.name} <${MIGRATOR_IDENTITY.email}>.`);
|
|
201
|
-
}
|
|
202
|
-
// We need the deltas for the Guard 3 summary BEFORE we decide to commit and
|
|
203
|
-
// push. To produce them we need the scratch clone in hand (so we can compare
|
|
204
|
-
// file-by-file). We clone into the scratch dir, take the diff, show the
|
|
205
|
-
// summary, confirm, then apply the delta via `cloneAndStackPush`.
|
|
206
|
-
//
|
|
207
|
-
// Rather than re-implement cloneAndStackPush's clone step here, we let it
|
|
208
|
-
// own the clone-commit-push pipeline and pass a deltas callback that runs
|
|
209
|
-
// AFTER the clone (it receives the localDir/scratchDir) but BEFORE the
|
|
210
|
-
// commit. `cloneAndStackPush` only exposes an `applyDelta` hook (write files
|
|
211
|
-
// then return), so we split the flow: first a probe clone to take the
|
|
212
|
-
// diff, then the real clone-and-stack-push. This is simpler than rewiring
|
|
213
|
-
// cloneAndStackPush's API.
|
|
214
|
-
// Probe clone.
|
|
215
|
-
await deps.gitOps.fetch(input.dir) // no-op for bare path if not a repo; deps.gitOps may short-circuit
|
|
216
|
-
.catch(() => {
|
|
217
|
-
// bare-path doesn't require a local fetch — swallow the error
|
|
218
|
-
});
|
|
219
|
-
// We don't have a probe clone helper; instead, ask the deps for a bare-path
|
|
220
|
-
// delta computation. The test double will produce this from a fixture; the
|
|
221
|
-
// real implementation walks both trees after ensuring a scratch clone exists.
|
|
222
|
-
// This is provided by the command glue layer.
|
|
223
|
-
const deltas = await deps.gitOps.diffHeadVsOrigin(input.dir);
|
|
224
|
-
const totalFilesInRemote = await deps.gitOps.countFilesInRemote(input.dir);
|
|
225
|
-
const summary = summarizeDiff(deltas, { totalFilesInRemote, unpinned });
|
|
226
|
-
log('');
|
|
227
|
-
log(summary.renderText({
|
|
228
|
-
workspaceSlug: backend.workspaceSlug,
|
|
229
|
-
repoUrl: stripAuth(backend.repoUrl),
|
|
230
|
-
branch: 'main',
|
|
231
|
-
localDir: input.dir,
|
|
232
|
-
}));
|
|
233
|
-
log('');
|
|
234
|
-
if (summary.totals.filesTouched === 0) {
|
|
235
|
-
return { kind: 'no-op', reason: 'Local tree matches remote HEAD. Nothing to push.' };
|
|
236
|
-
}
|
|
237
|
-
const ok = await decideConfirm({
|
|
238
|
-
destructive: summary.isDestructive,
|
|
239
|
-
requiresTypedSlug,
|
|
240
|
-
yes: input.yes,
|
|
241
|
-
workspaceSlug: backend.workspaceSlug,
|
|
242
|
-
confirmIO: deps.confirmIO,
|
|
243
|
-
});
|
|
244
|
-
if (!ok) {
|
|
245
|
-
log('Aborted, nothing pushed.');
|
|
246
|
-
return { kind: 'aborted', reason: 'user declined at confirm prompt' };
|
|
247
|
-
}
|
|
248
|
-
const baseMessage = input.message ?? `qfg push: ${summary.totals.filesTouched} file change(s)`;
|
|
249
|
-
const commitMessage = withPushedViaTrailer(baseMessage);
|
|
250
|
-
log('Pushing via clone-and-stack...');
|
|
251
|
-
try {
|
|
252
|
-
const result = await cloneAndStackPush({
|
|
253
|
-
remoteUrl: backend.repoUrl,
|
|
254
|
-
localDir: scratchDir,
|
|
255
|
-
author,
|
|
256
|
-
commitMessage,
|
|
257
|
-
async applyDelta(clonedDir) {
|
|
258
|
-
// The caller promised deps.copyDirMirror copies `input.dir` into
|
|
259
|
-
// `clonedDir`, skipping `.git/`. cloneAndStackPush will `git add --all`
|
|
260
|
-
// after this returns.
|
|
261
|
-
await deps.copyDirMirror(input.dir, clonedDir);
|
|
262
|
-
},
|
|
263
|
-
});
|
|
264
|
-
if (unpinned && !input.noPinWrite) {
|
|
265
|
-
try {
|
|
266
|
-
await writeWorkspaceSlug(input.dir, backend.workspaceSlug);
|
|
267
|
-
log(`Wrote workspace = "${backend.workspaceSlug}" into quonfig.json`);
|
|
268
|
-
}
|
|
269
|
-
catch {
|
|
270
|
-
// non-fatal
|
|
271
|
-
}
|
|
192
|
+
const toServerKind = (kind) => {
|
|
193
|
+
switch (kind) {
|
|
194
|
+
case 'added': {
|
|
195
|
+
return 'add';
|
|
272
196
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
catch (error) {
|
|
276
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
277
|
-
if (/non-fast-forward|fetch first|rejected/i.test(msg)) {
|
|
278
|
-
throw new PushFatalError('Remote has changes not in local — run `qfg pull` first, then re-run `qfg push`.', 'NON_FAST_FORWARD');
|
|
197
|
+
case 'deleted': {
|
|
198
|
+
return 'delete';
|
|
279
199
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
finally {
|
|
283
|
-
// Best-effort cleanup of the scratch clone. Ignore failures so a cleanup
|
|
284
|
-
// problem doesn't mask a real error.
|
|
285
|
-
try {
|
|
286
|
-
fs.rmSync(scratchDir, { recursive: true, force: true });
|
|
287
|
-
}
|
|
288
|
-
catch {
|
|
289
|
-
/* ignore */
|
|
200
|
+
case 'modified': {
|
|
201
|
+
return 'modify';
|
|
290
202
|
}
|
|
291
203
|
}
|
|
292
|
-
}
|
|
204
|
+
};
|
|
293
205
|
/**
|
|
294
206
|
* Map the guard outputs to the right prompt:
|
|
295
207
|
* - Typed-slug (always, never skipped by --yes) when identity demanded it
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
|
-
import * as os from 'node:os';
|
|
3
2
|
import * as path from 'node:path';
|
|
4
3
|
import { getDomain } from './domain-urls.js';
|
|
4
|
+
import { getQuonfigConfigHome } from './quonfig-home.js';
|
|
5
5
|
const BUFFER_MS = 5 * 60 * 1000; // 5 minutes
|
|
6
|
-
const getQuonfigDir = () =>
|
|
6
|
+
const getQuonfigDir = () => getQuonfigConfigHome();
|
|
7
7
|
const getGiteaTokenFile = () => {
|
|
8
8
|
const domain = getDomain();
|
|
9
9
|
const filename = domain === 'quonfig.com' ? 'gitea-tokens.json' : `gitea-tokens-${domain.replaceAll('.', '-')}.json`;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the directory the CLI uses for tokens, profile config, and gitea
|
|
3
|
+
* tokens. Honors `QUONFIG_CONFIG_HOME` so tests (and anyone who wants an
|
|
4
|
+
* isolated config dir) can redirect away from `~/.quonfig/`.
|
|
5
|
+
*/
|
|
6
|
+
export declare const getQuonfigConfigHome: () => string;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as os from 'node:os';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the directory the CLI uses for tokens, profile config, and gitea
|
|
5
|
+
* tokens. Honors `QUONFIG_CONFIG_HOME` so tests (and anyone who wants an
|
|
6
|
+
* isolated config dir) can redirect away from `~/.quonfig/`.
|
|
7
|
+
*/
|
|
8
|
+
export const getQuonfigConfigHome = () => process.env.QUONFIG_CONFIG_HOME || path.join(os.homedir(), '.quonfig');
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
|
-
import * as os from 'node:os';
|
|
3
2
|
import * as path from 'node:path';
|
|
4
3
|
import { getDomain } from './domain-urls.js';
|
|
5
|
-
|
|
4
|
+
import { getQuonfigConfigHome } from './quonfig-home.js';
|
|
5
|
+
const getQuonfigDir = (options) => options?.quonfigDir || getQuonfigConfigHome();
|
|
6
6
|
const getTokenFile = (options) => {
|
|
7
7
|
const domain = getDomain();
|
|
8
8
|
const filename = domain === 'quonfig.com' ? 'tokens.json' : `tokens-${domain.replaceAll('.', '-')}.json`;
|
package/dist/verify/validate.js
CHANGED
|
@@ -118,6 +118,7 @@ const VariantSchema = z.object({
|
|
|
118
118
|
value: ValueSchema,
|
|
119
119
|
description: z.string().optional(),
|
|
120
120
|
});
|
|
121
|
+
const AccessSchema = z.enum(['support', 'standard', 'protected-env', 'protected-all-envs']);
|
|
121
122
|
const StoredConfigSchema = z
|
|
122
123
|
.object({
|
|
123
124
|
id: z.string().optional(),
|
|
@@ -129,8 +130,7 @@ const StoredConfigSchema = z
|
|
|
129
130
|
description: z.string().optional(),
|
|
130
131
|
sendToClientSdk: z.boolean().optional(),
|
|
131
132
|
schemaKey: z.string().optional(),
|
|
132
|
-
|
|
133
|
-
protection: z.string().optional(),
|
|
133
|
+
access: AccessSchema.optional(),
|
|
134
134
|
default: z.object({
|
|
135
135
|
rules: z.array(ConfigRuleSchema),
|
|
136
136
|
}),
|
package/oclif.manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"packageManager": "yarn@4.11.0+sha512.4e54aeace9141df2f0177c266b05ec50dc044638157dae128c471ba65994ac802122d7ab35bcd9e81641228b7dcf24867d28e750e0bcae8a05277d600008ad54",
|
|
3
3
|
"name": "@quonfig/cli",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.23",
|
|
5
5
|
"bugs": {
|
|
6
6
|
"url": "https://github.com/quonfig/cli/issues"
|
|
7
7
|
},
|