@brandtg/flapjack 1.2.0 → 1.4.0

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/dist/cli.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import yargs from "yargs";
3
3
  import { hideBin } from "yargs/helpers";
4
4
  import { Pool } from "pg";
5
- import { FeatureFlagModel } from "./model.js";
5
+ import { FeatureFlagModel, FeatureFlagGroupModel } from "./model.js";
6
6
  import dotenv from "dotenv";
7
7
  dotenv.config({ quiet: true });
8
8
  function createDatabase() {
@@ -334,6 +334,740 @@ cli.command("is-active <name>", "Check if a feature flag is active for a user",
334
334
  await db.end();
335
335
  }
336
336
  });
337
+ // Check multiple flags for user command
338
+ cli.command("are-active", "Check if multiple feature flags are active for a user", (yargs) => {
339
+ return yargs.options({
340
+ names: {
341
+ type: "array",
342
+ describe: "Optional list of feature flag names (defaults to all flags)",
343
+ },
344
+ user: {
345
+ type: "string",
346
+ describe: "User ID",
347
+ },
348
+ roles: {
349
+ type: "array",
350
+ describe: "User roles",
351
+ },
352
+ groups: {
353
+ type: "array",
354
+ describe: "User groups",
355
+ },
356
+ });
357
+ }, async (argv) => {
358
+ const db = createDatabase();
359
+ const model = new FeatureFlagModel(db);
360
+ try {
361
+ const results = await model.areActiveForUser({
362
+ names: argv.names,
363
+ user: argv.user,
364
+ roles: argv.roles,
365
+ groups: argv.groups,
366
+ });
367
+ console.log(JSON.stringify({
368
+ names: argv.names,
369
+ user: argv.user,
370
+ roles: argv.roles,
371
+ groups: argv.groups,
372
+ results,
373
+ }, null, 2));
374
+ }
375
+ catch (error) {
376
+ console.error("Error checking multiple feature flags:", error);
377
+ process.exit(1);
378
+ }
379
+ finally {
380
+ await db.end();
381
+ }
382
+ });
383
+ // Check if active for context command
384
+ cli.command("is-active-context <name>", "Check if a feature flag is active for a context including subjects", (yargs) => {
385
+ return yargs
386
+ .positional("name", {
387
+ type: "string",
388
+ describe: "Feature flag name",
389
+ })
390
+ .options({
391
+ user: {
392
+ type: "string",
393
+ describe: "User ID",
394
+ },
395
+ roles: {
396
+ type: "array",
397
+ describe: "User roles",
398
+ },
399
+ groups: {
400
+ type: "array",
401
+ describe: "User groups",
402
+ },
403
+ subjects: {
404
+ type: "array",
405
+ describe: "External subject IDs (for example tenant:acme)",
406
+ },
407
+ });
408
+ }, async (argv) => {
409
+ const db = createDatabase();
410
+ const model = new FeatureFlagModel(db);
411
+ try {
412
+ const isActive = await model.isActiveForContext({
413
+ name: argv.name,
414
+ user: argv.user,
415
+ roles: argv.roles,
416
+ groups: argv.groups,
417
+ subjects: argv.subjects,
418
+ });
419
+ console.log(JSON.stringify({
420
+ name: argv.name,
421
+ isActive,
422
+ user: argv.user,
423
+ roles: argv.roles,
424
+ groups: argv.groups,
425
+ subjects: argv.subjects,
426
+ }, null, 2));
427
+ }
428
+ catch (error) {
429
+ console.error("Error checking feature flag context:", error);
430
+ process.exit(1);
431
+ }
432
+ finally {
433
+ await db.end();
434
+ }
435
+ });
436
+ // Add a subject to a feature flag
437
+ cli.command("add-subject <id> <subject>", "Add an external subject ID to a feature flag", (yargs) => {
438
+ return yargs
439
+ .positional("id", {
440
+ type: "number",
441
+ describe: "Feature flag ID",
442
+ })
443
+ .positional("subject", {
444
+ type: "string",
445
+ describe: "External subject ID (for example tenant:acme)",
446
+ });
447
+ }, async (argv) => {
448
+ const db = createDatabase();
449
+ const model = new FeatureFlagModel(db);
450
+ try {
451
+ const added = await model.addSubject(argv.id, argv.subject);
452
+ console.log(JSON.stringify({
453
+ id: argv.id,
454
+ subject: argv.subject,
455
+ added,
456
+ }, null, 2));
457
+ }
458
+ catch (error) {
459
+ console.error("Error adding subject:", error);
460
+ process.exit(1);
461
+ }
462
+ finally {
463
+ await db.end();
464
+ }
465
+ });
466
+ // Remove a subject from a feature flag
467
+ cli.command("remove-subject <id> <subject>", "Remove an external subject ID from a feature flag", (yargs) => {
468
+ return yargs
469
+ .positional("id", {
470
+ type: "number",
471
+ describe: "Feature flag ID",
472
+ })
473
+ .positional("subject", {
474
+ type: "string",
475
+ describe: "External subject ID",
476
+ });
477
+ }, async (argv) => {
478
+ const db = createDatabase();
479
+ const model = new FeatureFlagModel(db);
480
+ try {
481
+ const removed = await model.removeSubject(argv.id, argv.subject);
482
+ console.log(JSON.stringify({
483
+ id: argv.id,
484
+ subject: argv.subject,
485
+ removed,
486
+ }, null, 2));
487
+ }
488
+ catch (error) {
489
+ console.error("Error removing subject:", error);
490
+ process.exit(1);
491
+ }
492
+ finally {
493
+ await db.end();
494
+ }
495
+ });
496
+ // List subjects for a feature flag
497
+ cli.command("list-subjects <id>", "List external subject IDs for a feature flag", (yargs) => {
498
+ return yargs.positional("id", {
499
+ type: "number",
500
+ describe: "Feature flag ID",
501
+ });
502
+ }, async (argv) => {
503
+ const db = createDatabase();
504
+ const model = new FeatureFlagModel(db);
505
+ try {
506
+ const subjects = await model.getSubjects(argv.id);
507
+ console.log(JSON.stringify({
508
+ id: argv.id,
509
+ subjects,
510
+ }, null, 2));
511
+ }
512
+ catch (error) {
513
+ console.error("Error listing subjects:", error);
514
+ process.exit(1);
515
+ }
516
+ finally {
517
+ await db.end();
518
+ }
519
+ });
520
+ // Check multiple flags for context command
521
+ cli.command("are-active-context", "Check if multiple feature flags are active for a context including subjects", (yargs) => {
522
+ return yargs.options({
523
+ names: {
524
+ type: "array",
525
+ describe: "Optional list of feature flag names (defaults to all flags)",
526
+ },
527
+ user: {
528
+ type: "string",
529
+ describe: "User ID",
530
+ },
531
+ roles: {
532
+ type: "array",
533
+ describe: "User roles",
534
+ },
535
+ groups: {
536
+ type: "array",
537
+ describe: "User groups",
538
+ },
539
+ subjects: {
540
+ type: "array",
541
+ describe: "External subject IDs (for example tenant:acme)",
542
+ },
543
+ });
544
+ }, async (argv) => {
545
+ const db = createDatabase();
546
+ const model = new FeatureFlagModel(db);
547
+ try {
548
+ const results = await model.areActiveForContext({
549
+ names: argv.names,
550
+ user: argv.user,
551
+ roles: argv.roles,
552
+ groups: argv.groups,
553
+ subjects: argv.subjects,
554
+ });
555
+ console.log(JSON.stringify({
556
+ names: argv.names,
557
+ user: argv.user,
558
+ roles: argv.roles,
559
+ groups: argv.groups,
560
+ subjects: argv.subjects,
561
+ results,
562
+ }, null, 2));
563
+ }
564
+ catch (error) {
565
+ console.error("Error checking multiple feature flag contexts:", error);
566
+ process.exit(1);
567
+ }
568
+ finally {
569
+ await db.end();
570
+ }
571
+ });
572
+ // List flags that directly match a subject
573
+ cli.command("list-by-subject <subject>", "List feature flags directly assigned to an external subject ID", (yargs) => {
574
+ return yargs.positional("subject", {
575
+ type: "string",
576
+ describe: "External subject ID",
577
+ });
578
+ }, async (argv) => {
579
+ const db = createDatabase();
580
+ const model = new FeatureFlagModel(db);
581
+ try {
582
+ const flags = await model.getFeatureFlagsForSubject(argv.subject);
583
+ console.log(JSON.stringify({
584
+ subject: argv.subject,
585
+ flags,
586
+ }, null, 2));
587
+ }
588
+ catch (error) {
589
+ console.error("Error listing flags by subject:", error);
590
+ process.exit(1);
591
+ }
592
+ finally {
593
+ await db.end();
594
+ }
595
+ });
596
+ // Create feature flag group command
597
+ cli.command("group-create", "Create a feature flag group", (yargs) => {
598
+ return yargs
599
+ .options({
600
+ name: {
601
+ type: "string",
602
+ describe: "Feature flag group name",
603
+ },
604
+ note: {
605
+ type: "string",
606
+ describe: "Feature flag group description",
607
+ },
608
+ })
609
+ .demandOption("name", "Feature flag group name is required");
610
+ }, async (argv) => {
611
+ const db = createDatabase();
612
+ const model = new FeatureFlagGroupModel(db);
613
+ try {
614
+ const group = await model.create({
615
+ name: argv.name,
616
+ note: argv.note,
617
+ });
618
+ console.log(JSON.stringify(group, null, 2));
619
+ }
620
+ catch (error) {
621
+ console.error("Error creating feature flag group:", error);
622
+ process.exit(1);
623
+ }
624
+ finally {
625
+ await db.end();
626
+ }
627
+ });
628
+ // List feature flag groups command
629
+ cli.command("group-list", "List all feature flag groups", () => { }, async () => {
630
+ const db = createDatabase();
631
+ const model = new FeatureFlagGroupModel(db);
632
+ try {
633
+ const groups = await model.list();
634
+ console.log(JSON.stringify(groups, null, 2));
635
+ }
636
+ catch (error) {
637
+ console.error("Error listing feature flag groups:", error);
638
+ process.exit(1);
639
+ }
640
+ finally {
641
+ await db.end();
642
+ }
643
+ });
644
+ // Get feature flag group by ID command
645
+ cli.command("group-get <id>", "Get a feature flag group by ID", (yargs) => {
646
+ return yargs.positional("id", {
647
+ type: "number",
648
+ describe: "Feature flag group ID",
649
+ });
650
+ }, async (argv) => {
651
+ const db = createDatabase();
652
+ const model = new FeatureFlagGroupModel(db);
653
+ try {
654
+ const group = await model.getById(argv.id);
655
+ if (!group) {
656
+ console.log(`Feature flag group with ID ${argv.id} not found`);
657
+ process.exit(1);
658
+ }
659
+ console.log(JSON.stringify(group, null, 2));
660
+ }
661
+ catch (error) {
662
+ console.error("Error getting feature flag group:", error);
663
+ process.exit(1);
664
+ }
665
+ finally {
666
+ await db.end();
667
+ }
668
+ });
669
+ // Get feature flag group by name command
670
+ cli.command("group-get-by-name <name>", "Get a feature flag group by name", (yargs) => {
671
+ return yargs.positional("name", {
672
+ type: "string",
673
+ describe: "Feature flag group name",
674
+ });
675
+ }, async (argv) => {
676
+ const db = createDatabase();
677
+ const model = new FeatureFlagGroupModel(db);
678
+ try {
679
+ const group = await model.getByName(argv.name);
680
+ if (!group) {
681
+ console.log(`Feature flag group with name "${argv.name}" not found`);
682
+ process.exit(1);
683
+ }
684
+ console.log(JSON.stringify(group, null, 2));
685
+ }
686
+ catch (error) {
687
+ console.error("Error getting feature flag group by name:", error);
688
+ process.exit(1);
689
+ }
690
+ finally {
691
+ await db.end();
692
+ }
693
+ });
694
+ // Update feature flag group command
695
+ cli.command("group-update <id>", "Update a feature flag group", (yargs) => {
696
+ return yargs
697
+ .positional("id", {
698
+ type: "number",
699
+ describe: "Feature flag group ID",
700
+ })
701
+ .options({
702
+ name: {
703
+ type: "string",
704
+ describe: "Feature flag group name",
705
+ },
706
+ note: {
707
+ type: "string",
708
+ describe: "Feature flag group description",
709
+ },
710
+ "clear-note": {
711
+ type: "boolean",
712
+ describe: "Clear the group note",
713
+ },
714
+ });
715
+ }, async (argv) => {
716
+ const db = createDatabase();
717
+ const model = new FeatureFlagGroupModel(db);
718
+ try {
719
+ const changes = {};
720
+ if (argv.name !== undefined)
721
+ changes.name = argv.name;
722
+ if (argv.clearNote)
723
+ changes.note = null;
724
+ else if (argv.note !== undefined)
725
+ changes.note = argv.note;
726
+ const group = await model.update(argv.id, changes);
727
+ if (!group) {
728
+ console.log(`Feature flag group with ID ${argv.id} not found`);
729
+ process.exit(1);
730
+ }
731
+ console.log(JSON.stringify(group, null, 2));
732
+ }
733
+ catch (error) {
734
+ console.error("Error updating feature flag group:", error);
735
+ process.exit(1);
736
+ }
737
+ finally {
738
+ await db.end();
739
+ }
740
+ });
741
+ // Delete feature flag group command
742
+ cli.command("group-delete <id>", "Delete a feature flag group", (yargs) => {
743
+ return yargs.positional("id", {
744
+ type: "number",
745
+ describe: "Feature flag group ID",
746
+ });
747
+ }, async (argv) => {
748
+ const db = createDatabase();
749
+ const model = new FeatureFlagGroupModel(db);
750
+ try {
751
+ const deleted = await model.delete(argv.id);
752
+ if (!deleted) {
753
+ console.log(`Feature flag group with ID ${argv.id} not found`);
754
+ process.exit(1);
755
+ }
756
+ console.log(`Feature flag group with ID ${argv.id} deleted successfully`);
757
+ }
758
+ catch (error) {
759
+ console.error("Error deleting feature flag group:", error);
760
+ process.exit(1);
761
+ }
762
+ finally {
763
+ await db.end();
764
+ }
765
+ });
766
+ // Add flag to feature flag group command
767
+ cli.command("group-add-flag <groupId> <flagId>", "Add a feature flag to a group", (yargs) => {
768
+ return yargs
769
+ .positional("groupId", {
770
+ type: "number",
771
+ describe: "Feature flag group ID",
772
+ })
773
+ .positional("flagId", {
774
+ type: "number",
775
+ describe: "Feature flag ID",
776
+ });
777
+ }, async (argv) => {
778
+ const db = createDatabase();
779
+ const model = new FeatureFlagGroupModel(db);
780
+ try {
781
+ const added = await model.addFeatureFlag(argv.groupId, argv.flagId);
782
+ console.log(JSON.stringify({
783
+ groupId: argv.groupId,
784
+ flagId: argv.flagId,
785
+ added,
786
+ }, null, 2));
787
+ }
788
+ catch (error) {
789
+ console.error("Error adding feature flag to group:", error);
790
+ process.exit(1);
791
+ }
792
+ finally {
793
+ await db.end();
794
+ }
795
+ });
796
+ // Remove flag from feature flag group command
797
+ cli.command("group-remove-flag <groupId> <flagId>", "Remove a feature flag from a group", (yargs) => {
798
+ return yargs
799
+ .positional("groupId", {
800
+ type: "number",
801
+ describe: "Feature flag group ID",
802
+ })
803
+ .positional("flagId", {
804
+ type: "number",
805
+ describe: "Feature flag ID",
806
+ });
807
+ }, async (argv) => {
808
+ const db = createDatabase();
809
+ const model = new FeatureFlagGroupModel(db);
810
+ try {
811
+ const removed = await model.removeFeatureFlag(argv.groupId, argv.flagId);
812
+ console.log(JSON.stringify({
813
+ groupId: argv.groupId,
814
+ flagId: argv.flagId,
815
+ removed,
816
+ }, null, 2));
817
+ }
818
+ catch (error) {
819
+ console.error("Error removing feature flag from group:", error);
820
+ process.exit(1);
821
+ }
822
+ finally {
823
+ await db.end();
824
+ }
825
+ });
826
+ // List flags in feature flag group command
827
+ cli.command("group-list-flags <groupId>", "List all feature flags in a group", (yargs) => {
828
+ return yargs.positional("groupId", {
829
+ type: "number",
830
+ describe: "Feature flag group ID",
831
+ });
832
+ }, async (argv) => {
833
+ const db = createDatabase();
834
+ const model = new FeatureFlagGroupModel(db);
835
+ try {
836
+ const flags = await model.getFeatureFlags(argv.groupId);
837
+ console.log(JSON.stringify({
838
+ groupId: argv.groupId,
839
+ flags,
840
+ }, null, 2));
841
+ }
842
+ catch (error) {
843
+ console.error("Error listing flags for group:", error);
844
+ process.exit(1);
845
+ }
846
+ finally {
847
+ await db.end();
848
+ }
849
+ });
850
+ // List groups for feature flag command
851
+ cli.command("group-list-for-flag <flagId>", "List all groups that contain a feature flag", (yargs) => {
852
+ return yargs.positional("flagId", {
853
+ type: "number",
854
+ describe: "Feature flag ID",
855
+ });
856
+ }, async (argv) => {
857
+ const db = createDatabase();
858
+ const model = new FeatureFlagGroupModel(db);
859
+ try {
860
+ const groups = await model.getGroupsForFeatureFlag(argv.flagId);
861
+ console.log(JSON.stringify({
862
+ flagId: argv.flagId,
863
+ groups,
864
+ }, null, 2));
865
+ }
866
+ catch (error) {
867
+ console.error("Error listing groups for feature flag:", error);
868
+ process.exit(1);
869
+ }
870
+ finally {
871
+ await db.end();
872
+ }
873
+ });
874
+ // Bulk update flags in a group command
875
+ cli.command("group-update-all <groupId>", "Update all feature flags in a group", (yargs) => {
876
+ return yargs
877
+ .positional("groupId", {
878
+ type: "number",
879
+ describe: "Feature flag group ID",
880
+ })
881
+ .options({
882
+ everyone: {
883
+ type: "boolean",
884
+ describe: "Enable flag for everyone (overrides all other settings)",
885
+ },
886
+ percent: {
887
+ type: "number",
888
+ describe: "Percentage rollout (0-99.9)",
889
+ },
890
+ roles: {
891
+ type: "array",
892
+ describe: "List of roles that have this flag enabled",
893
+ },
894
+ groups: {
895
+ type: "array",
896
+ describe: "List of user groups that have this flag enabled",
897
+ },
898
+ users: {
899
+ type: "array",
900
+ describe: "List of specific user IDs that have this flag enabled",
901
+ },
902
+ "clear-everyone": {
903
+ type: "boolean",
904
+ describe: "Unset the everyone override",
905
+ },
906
+ "clear-percent": {
907
+ type: "boolean",
908
+ describe: "Unset percentage rollout",
909
+ },
910
+ "clear-roles": {
911
+ type: "boolean",
912
+ describe: "Clear roles list",
913
+ },
914
+ "clear-groups": {
915
+ type: "boolean",
916
+ describe: "Clear groups list",
917
+ },
918
+ "clear-users": {
919
+ type: "boolean",
920
+ describe: "Clear users list",
921
+ },
922
+ });
923
+ }, async (argv) => {
924
+ const db = createDatabase();
925
+ const model = new FeatureFlagGroupModel(db);
926
+ try {
927
+ const changes = {};
928
+ if (argv.clearEveryone)
929
+ changes.everyone = null;
930
+ else if (argv.everyone !== undefined)
931
+ changes.everyone = argv.everyone;
932
+ if (argv.clearPercent)
933
+ changes.percent = null;
934
+ else if (argv.percent !== undefined)
935
+ changes.percent = argv.percent;
936
+ if (argv.clearRoles)
937
+ changes.roles = null;
938
+ else if (argv.roles !== undefined)
939
+ changes.roles = argv.roles;
940
+ if (argv.clearGroups)
941
+ changes.groups = null;
942
+ else if (argv.groups !== undefined)
943
+ changes.groups = argv.groups;
944
+ if (argv.clearUsers)
945
+ changes.users = null;
946
+ else if (argv.users !== undefined)
947
+ changes.users = argv.users;
948
+ const updatedCount = await model.updateAll(argv.groupId, changes);
949
+ console.log(JSON.stringify({
950
+ groupId: argv.groupId,
951
+ updatedCount,
952
+ changes,
953
+ }, null, 2));
954
+ }
955
+ catch (error) {
956
+ console.error("Error updating all flags in group:", error);
957
+ process.exit(1);
958
+ }
959
+ finally {
960
+ await db.end();
961
+ }
962
+ });
963
+ // Add subject to feature flag group command
964
+ cli.command("group-add-subject <groupId> <subject>", "Add an external subject ID to a feature flag group", (yargs) => {
965
+ return yargs
966
+ .positional("groupId", {
967
+ type: "number",
968
+ describe: "Feature flag group ID",
969
+ })
970
+ .positional("subject", {
971
+ type: "string",
972
+ describe: "External subject ID",
973
+ });
974
+ }, async (argv) => {
975
+ const db = createDatabase();
976
+ const model = new FeatureFlagGroupModel(db);
977
+ try {
978
+ const added = await model.addSubject(argv.groupId, argv.subject);
979
+ console.log(JSON.stringify({
980
+ groupId: argv.groupId,
981
+ subject: argv.subject,
982
+ added,
983
+ }, null, 2));
984
+ }
985
+ catch (error) {
986
+ console.error("Error adding subject to group:", error);
987
+ process.exit(1);
988
+ }
989
+ finally {
990
+ await db.end();
991
+ }
992
+ });
993
+ // Remove subject from feature flag group command
994
+ cli.command("group-remove-subject <groupId> <subject>", "Remove an external subject ID from a feature flag group", (yargs) => {
995
+ return yargs
996
+ .positional("groupId", {
997
+ type: "number",
998
+ describe: "Feature flag group ID",
999
+ })
1000
+ .positional("subject", {
1001
+ type: "string",
1002
+ describe: "External subject ID",
1003
+ });
1004
+ }, async (argv) => {
1005
+ const db = createDatabase();
1006
+ const model = new FeatureFlagGroupModel(db);
1007
+ try {
1008
+ const removed = await model.removeSubject(argv.groupId, argv.subject);
1009
+ console.log(JSON.stringify({
1010
+ groupId: argv.groupId,
1011
+ subject: argv.subject,
1012
+ removed,
1013
+ }, null, 2));
1014
+ }
1015
+ catch (error) {
1016
+ console.error("Error removing subject from group:", error);
1017
+ process.exit(1);
1018
+ }
1019
+ finally {
1020
+ await db.end();
1021
+ }
1022
+ });
1023
+ // List subjects for feature flag group command
1024
+ cli.command("group-list-subjects <groupId>", "List external subject IDs for a feature flag group", (yargs) => {
1025
+ return yargs.positional("groupId", {
1026
+ type: "number",
1027
+ describe: "Feature flag group ID",
1028
+ });
1029
+ }, async (argv) => {
1030
+ const db = createDatabase();
1031
+ const model = new FeatureFlagGroupModel(db);
1032
+ try {
1033
+ const subjects = await model.getSubjects(argv.groupId);
1034
+ console.log(JSON.stringify({
1035
+ groupId: argv.groupId,
1036
+ subjects,
1037
+ }, null, 2));
1038
+ }
1039
+ catch (error) {
1040
+ console.error("Error listing subjects for group:", error);
1041
+ process.exit(1);
1042
+ }
1043
+ finally {
1044
+ await db.end();
1045
+ }
1046
+ });
1047
+ // List groups for subject command
1048
+ cli.command("group-list-by-subject <subject>", "List feature flag groups assigned to an external subject ID", (yargs) => {
1049
+ return yargs.positional("subject", {
1050
+ type: "string",
1051
+ describe: "External subject ID",
1052
+ });
1053
+ }, async (argv) => {
1054
+ const db = createDatabase();
1055
+ const model = new FeatureFlagGroupModel(db);
1056
+ try {
1057
+ const groups = await model.getGroupsForSubject(argv.subject);
1058
+ console.log(JSON.stringify({
1059
+ subject: argv.subject,
1060
+ groups,
1061
+ }, null, 2));
1062
+ }
1063
+ catch (error) {
1064
+ console.error("Error listing groups by subject:", error);
1065
+ process.exit(1);
1066
+ }
1067
+ finally {
1068
+ await db.end();
1069
+ }
1070
+ });
337
1071
  // Hash user ID command (utility)
338
1072
  cli.command("hash-user <userId>", "Hash a user ID using the same algorithm as percentage rollout", (yargs) => {
339
1073
  return yargs.positional("userId", {