@quonfig/cli 0.0.7 → 0.0.9

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 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.7 linux-x64 node-v24.4.1
17
+ @quonfig/cli/0.0.9 linux-x64 node-v24.4.1
18
18
  $ qfg --help [COMMAND]
19
19
  USAGE
20
20
  $ qfg COMMAND
@@ -27,6 +27,9 @@ USAGE
27
27
  <!-- commands -->
28
28
  * [`qfg config-schema`](#qfg-config-schema)
29
29
  * [`qfg create NAME`](#qfg-create-name)
30
+ * [`qfg flag info [NAME]`](#qfg-flag-info-name)
31
+ * [`qfg flag list`](#qfg-flag-list)
32
+ * [`qfg flag show [NAME]`](#qfg-flag-show-name)
30
33
  * [`qfg generate`](#qfg-generate)
31
34
  * [`qfg generate-new-hex-key`](#qfg-generate-new-hex-key)
32
35
  * [`qfg get [NAME]`](#qfg-get-name)
@@ -47,6 +50,7 @@ USAGE
47
50
  * [`qfg set-default [NAME]`](#qfg-set-default-name)
48
51
  * [`qfg set-rollout [NAME]`](#qfg-set-rollout-name)
49
52
  * [`qfg sync`](#qfg-sync)
53
+ * [`qfg toggle [NAME]`](#qfg-toggle-name)
50
54
  * [`qfg verify [PATH]`](#qfg-verify-path)
51
55
  * [`qfg whoami`](#qfg-whoami)
52
56
  * [`qfg workspace`](#qfg-workspace)
@@ -80,7 +84,7 @@ EXAMPLES
80
84
  $ qfg config-schema --json-schema # full JSON Schema document (copy into your editor)
81
85
  ```
82
86
 
83
- _See code: [src/commands/config-schema.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/config-schema.ts)_
87
+ _See code: [src/commands/config-schema.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/config-schema.ts)_
84
88
 
85
89
  ## `qfg create NAME`
86
90
 
@@ -152,7 +156,137 @@ EXAMPLES
152
156
  $ qfg set-rollout my.new.flag --environment production --true-percent 20
153
157
  ```
154
158
 
155
- _See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/create.ts)_
159
+ _See code: [src/commands/create.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/create.ts)_
160
+
161
+ ## `qfg flag info [NAME]`
162
+
163
+ Show current values, rules, and evaluation stats for a flag or config.
164
+
165
+ ```
166
+ USAGE
167
+ $ qfg flag info [NAME] [--json] [--interactive] [--no-color] [--verbose] [-w <value>] [--exclude-evaluations]
168
+
169
+ ARGUMENTS
170
+ NAME config/feature-flag/etc. name
171
+
172
+ FLAGS
173
+ --exclude-evaluations Exclude evaluation data
174
+
175
+ GLOBAL FLAGS
176
+ -w, --workspace=<value> Workspace slug to use (overrides QUONFIG_WORKSPACE env var and saved default)
177
+ --[no-]interactive Force interactive mode
178
+ --json Format output as json.
179
+ --no-color Do not colorize output
180
+ --verbose Verbose output
181
+
182
+ DESCRIPTION
183
+ Show current values, rules, and evaluation stats for a flag or config.
184
+
185
+ Output shows the value for each environment:
186
+ - A simple value (true / false / "hello") means a single unconditional rule.
187
+ - "[see rules]" means the environment has targeting rules or a percentage rollout.
188
+ - "[inherit]" means the environment falls back to the Default value.
189
+
190
+ Percentage rollouts are displayed as "20.0% true, 80.0% false".
191
+ Evaluation counts for the past 24 hours are included by default.
192
+
193
+ Related commands:
194
+ qfg set-default my.flag --environment production --value true # set a scalar fallback
195
+ qfg set-rollout my.flag --environment production --true-percent 20 # set a % rollout
196
+
197
+ ALIASES
198
+ $ qfg flag show
199
+ $ qfg flag info
200
+
201
+ EXAMPLES
202
+ $ qfg flag info my.config.name
203
+
204
+ $ qfg flag info my.config.name --exclude-evaluations # skip 24h stats
205
+ ```
206
+
207
+ ## `qfg flag list`
208
+
209
+ Show keys for your config/feature flags/etc.
210
+
211
+ ```
212
+ USAGE
213
+ $ qfg flag list [--json] [--interactive] [--no-color] [--verbose] [-w <value>] [--configs] [--feature-flags]
214
+ [--log-levels] [--schemas] [--segments]
215
+
216
+ FLAGS
217
+ --configs include configs
218
+ --feature-flags include flags
219
+ --log-levels include log levels
220
+ --schemas include schemas
221
+ --segments include segments
222
+
223
+ GLOBAL FLAGS
224
+ -w, --workspace=<value> Workspace slug to use (overrides QUONFIG_WORKSPACE env var and saved default)
225
+ --[no-]interactive Force interactive mode
226
+ --json Format output as json.
227
+ --no-color Do not colorize output
228
+ --verbose Verbose output
229
+
230
+ DESCRIPTION
231
+ Show keys for your config/feature flags/etc.
232
+
233
+ All types are returned by default. If you pass one or more type flags (e.g. --configs), only those types will be
234
+ returned
235
+
236
+ ALIASES
237
+ $ qfg flag list
238
+
239
+ EXAMPLES
240
+ $ qfg flag list
241
+
242
+ $ qfg flag list --feature-flags
243
+ ```
244
+
245
+ ## `qfg flag show [NAME]`
246
+
247
+ Show current values, rules, and evaluation stats for a flag or config.
248
+
249
+ ```
250
+ USAGE
251
+ $ qfg flag show [NAME] [--json] [--interactive] [--no-color] [--verbose] [-w <value>] [--exclude-evaluations]
252
+
253
+ ARGUMENTS
254
+ NAME config/feature-flag/etc. name
255
+
256
+ FLAGS
257
+ --exclude-evaluations Exclude evaluation data
258
+
259
+ GLOBAL FLAGS
260
+ -w, --workspace=<value> Workspace slug to use (overrides QUONFIG_WORKSPACE env var and saved default)
261
+ --[no-]interactive Force interactive mode
262
+ --json Format output as json.
263
+ --no-color Do not colorize output
264
+ --verbose Verbose output
265
+
266
+ DESCRIPTION
267
+ Show current values, rules, and evaluation stats for a flag or config.
268
+
269
+ Output shows the value for each environment:
270
+ - A simple value (true / false / "hello") means a single unconditional rule.
271
+ - "[see rules]" means the environment has targeting rules or a percentage rollout.
272
+ - "[inherit]" means the environment falls back to the Default value.
273
+
274
+ Percentage rollouts are displayed as "20.0% true, 80.0% false".
275
+ Evaluation counts for the past 24 hours are included by default.
276
+
277
+ Related commands:
278
+ qfg set-default my.flag --environment production --value true # set a scalar fallback
279
+ qfg set-rollout my.flag --environment production --true-percent 20 # set a % rollout
280
+
281
+ ALIASES
282
+ $ qfg flag show
283
+ $ qfg flag info
284
+
285
+ EXAMPLES
286
+ $ qfg flag show my.config.name
287
+
288
+ $ qfg flag show my.config.name --exclude-evaluations # skip 24h stats
289
+ ```
156
290
 
157
291
  ## `qfg generate`
158
292
 
@@ -222,7 +356,7 @@ EXAMPLES
222
356
  $ qfg generate --targets node-ts -o ./dist # combine with targets
223
357
  ```
224
358
 
225
- _See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/generate.ts)_
359
+ _See code: [src/commands/generate.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/generate.ts)_
226
360
 
227
361
  ## `qfg generate-new-hex-key`
228
362
 
@@ -245,7 +379,7 @@ EXAMPLES
245
379
  $ qfg generate-new-hex-key
246
380
  ```
247
381
 
248
- _See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/generate-new-hex-key.ts)_
382
+ _See code: [src/commands/generate-new-hex-key.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/generate-new-hex-key.ts)_
249
383
 
250
384
  ## `qfg get [NAME]`
251
385
 
@@ -277,7 +411,7 @@ EXAMPLES
277
411
  $ qfg get my.config.name --environment=production
278
412
  ```
279
413
 
280
- _See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/get.ts)_
414
+ _See code: [src/commands/get.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/get.ts)_
281
415
 
282
416
  ## `qfg info [NAME]`
283
417
 
@@ -315,13 +449,17 @@ DESCRIPTION
315
449
  qfg set-default my.flag --environment production --value true # set a scalar fallback
316
450
  qfg set-rollout my.flag --environment production --true-percent 20 # set a % rollout
317
451
 
452
+ ALIASES
453
+ $ qfg flag show
454
+ $ qfg flag info
455
+
318
456
  EXAMPLES
319
457
  $ qfg info my.config.name
320
458
 
321
459
  $ qfg info my.config.name --exclude-evaluations # skip 24h stats
322
460
  ```
323
461
 
324
- _See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/info.ts)_
462
+ _See code: [src/commands/info.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/info.ts)_
325
463
 
326
464
  ## `qfg init [DIRECTORY]`
327
465
 
@@ -359,7 +497,7 @@ EXAMPLES
359
497
  $ qfg init --dry-run
360
498
  ```
361
499
 
362
- _See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/init.ts)_
500
+ _See code: [src/commands/init.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/init.ts)_
363
501
 
364
502
  ## `qfg interactive`
365
503
 
@@ -377,7 +515,7 @@ EXAMPLES
377
515
  $ qfg
378
516
  ```
379
517
 
380
- _See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/interactive.ts)_
518
+ _See code: [src/commands/interactive.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/interactive.ts)_
381
519
 
382
520
  ## `qfg list`
383
521
 
@@ -408,13 +546,16 @@ DESCRIPTION
408
546
  All types are returned by default. If you pass one or more type flags (e.g. --configs), only those types will be
409
547
  returned
410
548
 
549
+ ALIASES
550
+ $ qfg flag list
551
+
411
552
  EXAMPLES
412
553
  $ qfg list
413
554
 
414
555
  $ qfg list --feature-flags
415
556
  ```
416
557
 
417
- _See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/list.ts)_
558
+ _See code: [src/commands/list.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/list.ts)_
418
559
 
419
560
  ## `qfg login`
420
561
 
@@ -439,7 +580,7 @@ EXAMPLES
439
580
  $ qfg login --profile myprofile
440
581
  ```
441
582
 
442
- _See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/login.ts)_
583
+ _See code: [src/commands/login.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/login.ts)_
443
584
 
444
585
  ## `qfg logout`
445
586
 
@@ -462,7 +603,7 @@ EXAMPLES
462
603
  $ qfg logout
463
604
  ```
464
605
 
465
- _See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/logout.ts)_
606
+ _See code: [src/commands/logout.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/logout.ts)_
466
607
 
467
608
  ## `qfg mcp`
468
609
 
@@ -495,7 +636,7 @@ EXAMPLES
495
636
  $ qfg mcp --url http://local-app.quonfig-staging.com:3003/api/v1/mcp
496
637
  ```
497
638
 
498
- _See code: [src/commands/mcp.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/mcp.ts)_
639
+ _See code: [src/commands/mcp.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/mcp.ts)_
499
640
 
500
641
  ## `qfg override [NAME]`
501
642
 
@@ -534,7 +675,7 @@ EXAMPLES
534
675
  $ qfg override my.double.config --value=3.14159
535
676
  ```
536
677
 
537
- _See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/override.ts)_
678
+ _See code: [src/commands/override.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/override.ts)_
538
679
 
539
680
  ## `qfg pull`
540
681
 
@@ -578,7 +719,7 @@ EXAMPLES
578
719
  $ qfg pull # uses QUONFIG_DIR env var
579
720
  ```
580
721
 
581
- _See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/pull.ts)_
722
+ _See code: [src/commands/pull.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/pull.ts)_
582
723
 
583
724
  ## `qfg schema NAME`
584
725
 
@@ -616,7 +757,7 @@ EXAMPLES
616
757
  $ qfg schema my-schema --set-json-schema=@schemas/my-schema.json --protected
617
758
  ```
618
759
 
619
- _See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/schema.ts)_
760
+ _See code: [src/commands/schema.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/schema.ts)_
620
761
 
621
762
  ## `qfg sdk-key`
622
763
 
@@ -643,7 +784,7 @@ EXAMPLES
643
784
  $ qfg sdk-key revoke <key-id>
644
785
  ```
645
786
 
646
- _See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/sdk-key.ts)_
787
+ _See code: [src/commands/sdk-key.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/sdk-key.ts)_
647
788
 
648
789
  ## `qfg sdk-key create`
649
790
 
@@ -675,7 +816,7 @@ EXAMPLES
675
816
  $ qfg sdk-key create --environment staging --type browser
676
817
  ```
677
818
 
678
- _See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/sdk-key/create.ts)_
819
+ _See code: [src/commands/sdk-key/create.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/sdk-key/create.ts)_
679
820
 
680
821
  ## `qfg sdk-key list`
681
822
 
@@ -704,7 +845,7 @@ EXAMPLES
704
845
  $ qfg sdk-key list --environment production
705
846
  ```
706
847
 
707
- _See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/sdk-key/list.ts)_
848
+ _See code: [src/commands/sdk-key/list.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/sdk-key/list.ts)_
708
849
 
709
850
  ## `qfg sdk-key revoke KEYID`
710
851
 
@@ -731,7 +872,7 @@ EXAMPLES
731
872
  $ qfg sdk-key revoke a1b2c3d4-e5f6-7890-abcd-ef1234567890
732
873
  ```
733
874
 
734
- _See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/sdk-key/revoke.ts)_
875
+ _See code: [src/commands/sdk-key/revoke.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/sdk-key/revoke.ts)_
735
876
 
736
877
  ## `qfg set-default [NAME]`
737
878
 
@@ -779,6 +920,9 @@ DESCRIPTION
779
920
  To see all current values and rules for a flag:
780
921
  qfg info my.flag
781
922
 
923
+ ALIASES
924
+ $ qfg toggle
925
+
782
926
  EXAMPLES
783
927
  $ qfg set-default my.flag.name # prompts for value and env
784
928
 
@@ -795,7 +939,7 @@ EXAMPLES
795
939
  $ qfg set-rollout my.flag.name --environment production --true-percent 20
796
940
  ```
797
941
 
798
- _See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/set-default.ts)_
942
+ _See code: [src/commands/set-default.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/set-default.ts)_
799
943
 
800
944
  ## `qfg set-rollout [NAME]`
801
945
 
@@ -872,7 +1016,7 @@ EXAMPLES
872
1016
  $ qfg set-rollout my.variant.flag --environment production --weights "a:33,b:33,c:34" --hash-by user.id
873
1017
  ```
874
1018
 
875
- _See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/set-rollout.ts)_
1019
+ _See code: [src/commands/set-rollout.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/set-rollout.ts)_
876
1020
 
877
1021
  ## `qfg sync`
878
1022
 
@@ -904,7 +1048,72 @@ EXAMPLES
904
1048
  $ qfg sync --watch --dir ./our-config --interval 30
905
1049
  ```
906
1050
 
907
- _See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/sync.ts)_
1051
+ _See code: [src/commands/sync.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/sync.ts)_
1052
+
1053
+ ## `qfg toggle [NAME]`
1054
+
1055
+ Set the unconditional fallback value for a flag or config in a specific environment.
1056
+
1057
+ ```
1058
+ USAGE
1059
+ $ qfg toggle [NAME] [--json] [--interactive] [--no-color] [--verbose] [-w <value>] [--confidential]
1060
+ [--env-var <value>] [--environment <value>] [--value <value>] [--confirm] [--secret] [--secret-key-name <value>]
1061
+
1062
+ ARGUMENTS
1063
+ NAME config/feature-flag/etc. name
1064
+
1065
+ FLAGS
1066
+ --confidential mark the value as confidential
1067
+ --confirm confirm without prompt
1068
+ --env-var=<value> environment variable to use as default value
1069
+ --environment=<value> environment to change
1070
+ --secret encrypt the value of this item
1071
+ --secret-key-name=<value> [default: quonfig.secrets.encryption.key] name of the secret key to use for
1072
+ encryption/decryption
1073
+ --value=<value> new default value
1074
+
1075
+ GLOBAL FLAGS
1076
+ -w, --workspace=<value> Workspace slug to use (overrides QUONFIG_WORKSPACE env var and saved default)
1077
+ --[no-]interactive Force interactive mode
1078
+ --json Format output as json.
1079
+ --no-color Do not colorize output
1080
+ --verbose Verbose output
1081
+
1082
+ DESCRIPTION
1083
+ Set the unconditional fallback value for a flag or config in a specific environment.
1084
+
1085
+ This updates the catch-all rule — the value users receive when NO targeting rule matches.
1086
+ Any targeting rules or percentage rollouts you have configured are NOT affected; they
1087
+ continue to fire before this fallback is evaluated.
1088
+
1089
+ "Other rules still apply" means: if you have rules targeting specific users, segments,
1090
+ or properties, those rules still take priority. This command only changes what everyone
1091
+ else sees.
1092
+
1093
+ To set a percentage rollout (gradual rollout / A/B test / canary deploy) instead:
1094
+ qfg set-rollout my.flag --environment production --true-percent 20
1095
+
1096
+ To see all current values and rules for a flag:
1097
+ qfg info my.flag
1098
+
1099
+ ALIASES
1100
+ $ qfg toggle
1101
+
1102
+ EXAMPLES
1103
+ $ qfg toggle my.flag.name # prompts for value and env
1104
+
1105
+ $ qfg toggle my.flag.name --value=true --environment=staging
1106
+
1107
+ $ qfg toggle my.flag.name --value=false --environment=production # kill-switch: turn off for everyone
1108
+
1109
+ $ qfg toggle my.flag.name --value=true --secret
1110
+
1111
+ $ qfg toggle my.config.name --env-var=MY_ENV_VAR_NAME --environment=production
1112
+
1113
+ # For a percentage rollout, use set-rollout instead:
1114
+
1115
+ $ qfg set-rollout my.flag.name --environment production --true-percent 20
1116
+ ```
908
1117
 
909
1118
  ## `qfg verify [PATH]`
910
1119
 
@@ -937,7 +1146,7 @@ EXAMPLES
937
1146
  $ qfg verify --json
938
1147
  ```
939
1148
 
940
- _See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/verify.ts)_
1149
+ _See code: [src/commands/verify.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/verify.ts)_
941
1150
 
942
1151
  ## `qfg whoami`
943
1152
 
@@ -960,7 +1169,7 @@ EXAMPLES
960
1169
  $ qfg whoami
961
1170
  ```
962
1171
 
963
- _See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/whoami.ts)_
1172
+ _See code: [src/commands/whoami.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/whoami.ts)_
964
1173
 
965
1174
  ## `qfg workspace`
966
1175
 
@@ -983,7 +1192,7 @@ EXAMPLES
983
1192
  $ qfg workspace
984
1193
  ```
985
1194
 
986
- _See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/workspace.ts)_
1195
+ _See code: [src/commands/workspace.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/workspace.ts)_
987
1196
 
988
1197
  ## `qfg workspace bootstrap`
989
1198
 
@@ -1016,7 +1225,7 @@ EXAMPLES
1016
1225
  $ qfg workspace bootstrap --dir ./our-config --skip-validate
1017
1226
  ```
1018
1227
 
1019
- _See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/workspace/bootstrap.ts)_
1228
+ _See code: [src/commands/workspace/bootstrap.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/workspace/bootstrap.ts)_
1020
1229
 
1021
1230
  ## `qfg workspace switch`
1022
1231
 
@@ -1039,7 +1248,7 @@ EXAMPLES
1039
1248
  $ qfg workspace switch
1040
1249
  ```
1041
1250
 
1042
- _See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.0.7/src/commands/workspace/switch.ts)_
1251
+ _See code: [src/commands/workspace/switch.ts](https://github.com/quonfig/cli/blob/v0.0.9/src/commands/workspace/switch.ts)_
1043
1252
  <!-- commandsstop -->
1044
1253
 
1045
1254
  ## Local Development
@@ -56,7 +56,7 @@ export default class Get extends APICommand {
56
56
  }
57
57
  const request = await this.apiClient.post('/api/v1/evaluations/evaluate', {
58
58
  workspaceId: this.workspaceId,
59
- environmentId: environment.id,
59
+ environmentName: environment.name,
60
60
  context: {},
61
61
  });
62
62
  if (!request.ok) {
@@ -1,6 +1,7 @@
1
1
  import { APICommand } from '../index.js';
2
2
  import { JsonObj } from '../result.js';
3
3
  export default class Info extends APICommand {
4
+ static aliases: string[];
4
5
  static args: {
5
6
  name: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
6
7
  };
@@ -6,6 +6,7 @@ import { getAppUrl } from '../util/domain-urls.js';
6
6
  import isInteractive from '../util/is-interactive.js';
7
7
  import nameArg from '../util/name-arg.js';
8
8
  export default class Info extends APICommand {
9
+ static aliases = ['flag:show', 'flag:info'];
9
10
  static args = { ...nameArg };
10
11
  static description = `Show current values, rules, and evaluation stats for a flag or config.
11
12
 
@@ -1,5 +1,6 @@
1
1
  import { APICommand } from '../index.js';
2
2
  export default class List extends APICommand {
3
+ static aliases: string[];
3
4
  static description: string;
4
5
  static examples: string[];
5
6
  static flags: {
@@ -1,6 +1,7 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { APICommand } from '../index.js';
3
3
  export default class List extends APICommand {
4
+ static aliases = ['flag:list'];
4
5
  static description = `Show keys for your config/feature flags/etc.
5
6
 
6
7
  All types are returned by default. If you pass one or more type flags (e.g. --configs), only those types will be returned`;
@@ -1,6 +1,7 @@
1
1
  import { APICommand } from '../index.js';
2
2
  import { JsonObj } from '../result.js';
3
3
  export default class SetDefault extends APICommand {
4
+ static aliases: string[];
4
5
  static args: {
5
6
  name: import("@oclif/core/lib/interfaces/parser.js").Arg<string | undefined, Record<string, unknown>>;
6
7
  };
@@ -13,6 +13,7 @@ import isInteractive from '../util/is-interactive.js';
13
13
  import nameArg from '../util/name-arg.js';
14
14
  import secretFlags, { parsedSecretFlags } from '../util/secret-flags.js';
15
15
  export default class SetDefault extends APICommand {
16
+ static aliases = ['toggle'];
16
17
  static args = { ...nameArg };
17
18
  static description = `Set the unconditional fallback value for a flag or config in a specific environment.
18
19
 
@@ -17,3 +17,21 @@ export default class SetRollout extends APICommand {
17
17
  private parseWeights;
18
18
  private coerceValue;
19
19
  }
20
+ /**
21
+ * Pick a short, stable variant name from a rollout value. Uses the first
22
+ * 10 characters of the stringified value, then dedupes collisions by
23
+ * appending `-2`, `-3`, ... so each name is unique within the set.
24
+ */
25
+ export declare function synthesizeVariants(weightedValues: Array<{
26
+ value: {
27
+ type: string;
28
+ value: unknown;
29
+ };
30
+ weight: number;
31
+ }>): Array<{
32
+ name: string;
33
+ value: {
34
+ type: string;
35
+ value: unknown;
36
+ };
37
+ }>;
@@ -165,7 +165,20 @@ To inspect the current rollout:
165
165
  const updatedEnvironments = hasEnv
166
166
  ? existingEnvs.map((e) => (e.id === envKey ? { ...e, rules: [newRule] } : e))
167
167
  : [...existingEnvs, { id: envKey, rules: [newRule] }];
168
- const updateFields = { environments: updatedEnvironments };
168
+ // Auto-create variants when the config has none. The UI cannot render a
169
+ // rollout without named variants, and the server/gitea verify now rejects
170
+ // it. Bool configs are exempt — the UI supplies implicit true/false variants.
171
+ const existingVariants = currentConfig.variants ?? [];
172
+ const updateFields = {
173
+ environments: updatedEnvironments,
174
+ };
175
+ if (existingVariants.length === 0 && config.valueType !== 'bool') {
176
+ const synthesized = synthesizeVariants(weightedValues);
177
+ if (synthesized.length > 0) {
178
+ updateFields.variants = synthesized;
179
+ this.log(`${checkmark} Auto-created ${synthesized.length} variant${synthesized.length === 1 ? '' : 's'}: ${synthesized.map((v) => v.name).join(', ')}`);
180
+ }
181
+ }
169
182
  this.verboseLog('Update fields:', JSON.stringify(updateFields, null, 2));
170
183
  let request;
171
184
  if (config.type === 'feature_flag') {
@@ -229,32 +242,57 @@ To inspect the current rollout:
229
242
  return { ok: true, weights };
230
243
  }
231
244
  coerceValue(raw, valueType) {
232
- const type = valueType.toLowerCase();
233
- switch (type) {
234
- case 'bool': {
235
- if (raw !== 'true' && raw !== 'false') {
236
- return { error: `Invalid boolean value "${raw}". Must be "true" or "false".`, ok: false };
237
- }
238
- return { ok: true, typed: { type: 'bool', value: raw === 'true' } };
239
- }
240
- case 'int': {
241
- const n = Number.parseInt(raw, 10);
242
- if (Number.isNaN(n)) {
243
- return { error: `Invalid int value "${raw}".`, ok: false };
244
- }
245
- return { ok: true, typed: { type: 'int', value: n } };
245
+ return coerceValue(raw, valueType);
246
+ }
247
+ }
248
+ /**
249
+ * Pick a short, stable variant name from a rollout value. Uses the first
250
+ * 10 characters of the stringified value, then dedupes collisions by
251
+ * appending `-2`, `-3`, ... so each name is unique within the set.
252
+ */
253
+ export function synthesizeVariants(weightedValues) {
254
+ const used = new Set();
255
+ const variants = [];
256
+ for (const wv of weightedValues) {
257
+ const raw = typeof wv.value.value === 'string' ? wv.value.value : JSON.stringify(wv.value.value);
258
+ const base = (raw || wv.value.type).slice(0, 10);
259
+ let name = base;
260
+ let suffix = 2;
261
+ while (used.has(name)) {
262
+ name = `${base}-${suffix}`;
263
+ suffix += 1;
264
+ }
265
+ used.add(name);
266
+ variants.push({ name, value: wv.value });
267
+ }
268
+ return variants;
269
+ }
270
+ function coerceValue(raw, valueType) {
271
+ const type = valueType.toLowerCase();
272
+ switch (type) {
273
+ case 'bool': {
274
+ if (raw !== 'true' && raw !== 'false') {
275
+ return { error: `Invalid boolean value "${raw}". Must be "true" or "false".`, ok: false };
246
276
  }
247
- case 'double': {
248
- const n = Number.parseFloat(raw);
249
- if (Number.isNaN(n)) {
250
- return { error: `Invalid double value "${raw}".`, ok: false };
251
- }
252
- return { ok: true, typed: { type: 'double', value: n } };
277
+ return { ok: true, typed: { type: 'bool', value: raw === 'true' } };
278
+ }
279
+ case 'int': {
280
+ const n = Number.parseInt(raw, 10);
281
+ if (Number.isNaN(n)) {
282
+ return { error: `Invalid int value "${raw}".`, ok: false };
253
283
  }
254
- default: {
255
- // string, json, etc — pass as-is
256
- return { ok: true, typed: { type, value: raw } };
284
+ return { ok: true, typed: { type: 'int', value: n } };
285
+ }
286
+ case 'double': {
287
+ const n = Number.parseFloat(raw);
288
+ if (Number.isNaN(n)) {
289
+ return { error: `Invalid double value "${raw}".`, ok: false };
257
290
  }
291
+ return { ok: true, typed: { type: 'double', value: n } };
292
+ }
293
+ default: {
294
+ // string, json, etc — pass as-is
295
+ return { ok: true, typed: { type, value: raw } };
258
296
  }
259
297
  }
260
298
  }
@@ -0,0 +1,3 @@
1
+ import { Hook } from '@oclif/core';
2
+ declare const hook: Hook<'init'>;
3
+ export default hook;
@@ -0,0 +1,28 @@
1
+ import { appendFrictionEntry, buildFrictionEntry, extractLastErrorLine, getFrictionLogPath, installStderrCapture, } from '../../util/friction-log.js';
2
+ let installed = false;
3
+ const hook = async function ({ config: _config }) {
4
+ if (installed)
5
+ return;
6
+ installed = true;
7
+ const logPath = getFrictionLogPath(process.env.QFG_FRICTION_LOG);
8
+ if (!logPath)
9
+ return;
10
+ const readCapturedStderr = installStderrCapture();
11
+ const originalArgv = process.argv.slice(2);
12
+ process.on('exit', (code) => {
13
+ if (code === 0)
14
+ return;
15
+ try {
16
+ const entry = buildFrictionEntry({
17
+ argv: originalArgv,
18
+ error: extractLastErrorLine(readCapturedStderr()),
19
+ exitCode: code,
20
+ });
21
+ appendFrictionEntry(logPath, entry);
22
+ }
23
+ catch {
24
+ // never fail a CLI invocation because of friction logging
25
+ }
26
+ });
27
+ };
28
+ export default hook;
@@ -41,7 +41,6 @@ export const SAMPLE_FILES = [
41
41
  key: 'example.dark-mode',
42
42
  type: 'feature_flag',
43
43
  valueType: 'bool',
44
- sendToClientSdk: true,
45
44
  description: 'Enable dark mode UI.',
46
45
  default: {
47
46
  rules: [
@@ -0,0 +1,17 @@
1
+ export interface FrictionEntry {
2
+ attempted: string;
3
+ error: string;
4
+ exitCode: number;
5
+ ts: string;
6
+ }
7
+ export declare function getFrictionLogPath(envValue: string | undefined, cwd?: string): null | string;
8
+ export declare function buildFrictionEntry(opts: {
9
+ argv?: string[];
10
+ binName?: string;
11
+ error?: string;
12
+ exitCode: number;
13
+ now?: Date;
14
+ }): FrictionEntry;
15
+ export declare function appendFrictionEntry(logPath: string, entry: FrictionEntry): void;
16
+ export declare function extractLastErrorLine(stderr: string): string | undefined;
17
+ export declare function installStderrCapture(): () => string;
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ export function getFrictionLogPath(envValue, cwd = process.cwd()) {
4
+ if (envValue === undefined)
5
+ return null;
6
+ const trimmed = envValue.trim();
7
+ if (trimmed === '' || trimmed === 'false' || trimmed === '0')
8
+ return null;
9
+ if (trimmed === 'true' || trimmed === '1') {
10
+ return path.join(cwd, '.qfg-friction.log');
11
+ }
12
+ return path.isAbsolute(trimmed) ? trimmed : path.resolve(cwd, trimmed);
13
+ }
14
+ export function buildFrictionEntry(opts) {
15
+ const argv = opts.argv ?? process.argv.slice(2);
16
+ const binName = opts.binName ?? 'qfg';
17
+ const error = opts.error?.trim() || `nonzero exit code ${opts.exitCode}`;
18
+ return {
19
+ attempted: [binName, ...argv].join(' '),
20
+ error,
21
+ exitCode: opts.exitCode,
22
+ ts: (opts.now ?? new Date()).toISOString(),
23
+ };
24
+ }
25
+ export function appendFrictionEntry(logPath, entry) {
26
+ fs.appendFileSync(logPath, JSON.stringify(entry) + '\n', 'utf8');
27
+ }
28
+ const MAX_STDERR_CAPTURE = 4000;
29
+ export function extractLastErrorLine(stderr) {
30
+ if (!stderr)
31
+ return undefined;
32
+ const stripped = stderr.replaceAll(/\u001B\[[\d;]*[A-Za-z]/g, '');
33
+ const lines = stripped.split('\n').map((l) => l.trim()).filter(Boolean);
34
+ if (lines.length === 0)
35
+ return undefined;
36
+ const errorLine = [...lines].reverse().find((l) => /^(error|»|×|✖)/i.test(l));
37
+ return (errorLine ?? lines.at(-1))?.replace(/^»\s*/, '').replace(/^error:\s*/i, '');
38
+ }
39
+ export function installStderrCapture() {
40
+ let captured = '';
41
+ const origWrite = process.stderr.write.bind(process.stderr);
42
+ process.stderr.write = ((chunk, ...rest) => {
43
+ try {
44
+ const asString = typeof chunk === 'string' ? chunk : chunk.toString();
45
+ captured += asString;
46
+ if (captured.length > MAX_STDERR_CAPTURE) {
47
+ captured = captured.slice(-MAX_STDERR_CAPTURE);
48
+ }
49
+ }
50
+ catch {
51
+ // ignore — stderr capture is best-effort
52
+ }
53
+ return origWrite(chunk, ...rest);
54
+ });
55
+ return () => captured;
56
+ }
@@ -142,7 +142,7 @@ const StoredConfigSchema = z.object({
142
142
  valueType: ValueTypeSchema,
143
143
  name: z.string().optional(),
144
144
  description: z.string().optional(),
145
- sendToClientSdk: z.boolean().default(false),
145
+ sendToClientSdk: z.boolean().optional(),
146
146
  schemaKey: z.string().optional(),
147
147
  accessLevel: z.string().optional(),
148
148
  protection: z.string().optional(),
@@ -183,6 +183,24 @@ const VALUE_REQUIRED_OPERATORS = new Set([
183
183
  "IN_INT_RANGE",
184
184
  "LOOKUP_KEY_IN", "LOOKUP_KEY_NOT_IN",
185
185
  ]);
186
+ /**
187
+ * Reject `sendToClientSdk` on feature_flag rows — the field is config-only.
188
+ * Checks the raw parsed JSON so the key is forbidden regardless of value
189
+ * (true or false). Segments are handled separately on the parsed config.
190
+ */
191
+ function checkFeatureFlagForbiddenFields(parsed, file, issues) {
192
+ if (typeof parsed === "object" &&
193
+ parsed !== null &&
194
+ parsed.type === "feature_flag" &&
195
+ Object.prototype.hasOwnProperty.call(parsed, "sendToClientSdk")) {
196
+ issues.push({
197
+ file,
198
+ message: `feature_flag must not set "sendToClientSdk" — this field is only valid on config rows`,
199
+ severity: "error",
200
+ suggestion: `Remove the "sendToClientSdk" field from this feature_flag JSON`,
201
+ });
202
+ }
203
+ }
186
204
  // ── Validation from filesystem ──────────────────────────────────────────
187
205
  import * as fs from "node:fs";
188
206
  import * as path from "node:path";
@@ -334,6 +352,7 @@ export function validateWorkspace(workspaceDir) {
334
352
  stats.schemas++;
335
353
  continue;
336
354
  }
355
+ checkFeatureFlagForbiddenFields(parsed, relPath, issues);
337
356
  // Validate against StoredConfigSchema
338
357
  const result = StoredConfigSchema.safeParse(parsed);
339
358
  if (!result.success) {
@@ -631,6 +650,7 @@ export function validateFileMap(files) {
631
650
  allSchemaFiles.push({ key: schemaKey, file: relPath });
632
651
  continue;
633
652
  }
653
+ checkFeatureFlagForbiddenFields(parsed, relPath, issues);
634
654
  const result = StoredConfigSchema.safeParse(parsed);
635
655
  if (!result.success) {
636
656
  for (const issue of result.error.issues) {
@@ -835,6 +855,19 @@ function validateRules(rules, file, context, expectedValueType, issues, variants
835
855
  if (wv.weightedValues.length === 0) {
836
856
  issues.push({ file, message: `${ruleCtx}: weighted values list is empty`, severity: "error" });
837
857
  }
858
+ // Percentage rollouts must reference named variants — the UI cannot edit
859
+ // a rollout when variants is empty. Bool configs are exempt because the
860
+ // UI supplies implicit true/false variants.
861
+ if (wv.weightedValues.length > 0 &&
862
+ variants.length === 0 &&
863
+ expectedValueType !== "bool") {
864
+ issues.push({
865
+ file,
866
+ message: `${ruleCtx}: weighted_values rollout requires at least one variant, but variants is empty`,
867
+ severity: "error",
868
+ suggestion: `Define variants for the possible rollout values, then reference them from weightedValues.`,
869
+ });
870
+ }
838
871
  for (let j = 0; j < wv.weightedValues.length; j++) {
839
872
  if (wv.weightedValues[j].value.type !== expectedValueType) {
840
873
  issues.push({
@@ -421,7 +421,10 @@
421
421
  ]
422
422
  },
423
423
  "info": {
424
- "aliases": [],
424
+ "aliases": [
425
+ "flag:show",
426
+ "flag:info"
427
+ ],
425
428
  "args": {
426
429
  "name": {
427
430
  "description": "config/feature-flag/etc. name",
@@ -634,7 +637,9 @@
634
637
  ]
635
638
  },
636
639
  "list": {
637
- "aliases": [],
640
+ "aliases": [
641
+ "flag:list"
642
+ ],
638
643
  "args": {},
639
644
  "description": "Show keys for your config/feature flags/etc.\n\n All types are returned by default. If you pass one or more type flags (e.g. --configs), only those types will be returned",
640
645
  "examples": [
@@ -1315,7 +1320,9 @@
1315
1320
  ]
1316
1321
  },
1317
1322
  "set-default": {
1318
- "aliases": [],
1323
+ "aliases": [
1324
+ "toggle"
1325
+ ],
1319
1326
  "args": {
1320
1327
  "name": {
1321
1328
  "description": "config/feature-flag/etc. name",
@@ -2210,5 +2217,5 @@
2210
2217
  ]
2211
2218
  }
2212
2219
  },
2213
- "version": "0.0.7"
2220
+ "version": "0.0.9"
2214
2221
  }
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.7",
4
+ "version": "0.0.9",
5
5
  "bugs": {
6
6
  "url": "https://github.com/quonfig/cli/issues"
7
7
  },
@@ -81,7 +81,10 @@
81
81
  "plugins": [],
82
82
  "topicSeparator": " ",
83
83
  "helpClass": "./dist/help",
84
- "stacktrace": false
84
+ "stacktrace": false,
85
+ "hooks": {
86
+ "init": "./dist/hooks/init/friction-log"
87
+ }
85
88
  },
86
89
  "scripts": {
87
90
  "build": "shx rm -rf dist && tsc -b",