@brandtg/flapjack 1.3.0 → 1.5.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() {
@@ -47,6 +47,10 @@ const flagOptions = {
47
47
  type: "array",
48
48
  describe: "List of specific user IDs that have this flag enabled",
49
49
  },
50
+ tags: {
51
+ type: "array",
52
+ describe: "List of tags for organizing this flag (e.g. release:1.5.0)",
53
+ },
50
54
  note: {
51
55
  type: "string",
52
56
  describe: "Description of where this flag is used and what it does",
@@ -56,6 +60,14 @@ const flagOptions = {
56
60
  describe: "Expiration date (ISO 8601 format)",
57
61
  },
58
62
  };
63
+ // Option to include archived (hidden) records in read commands
64
+ const includeArchivedOption = {
65
+ "include-archived": {
66
+ type: "boolean",
67
+ default: false,
68
+ describe: "Include archived (hidden) records in the results",
69
+ },
70
+ };
59
71
  // Create command
60
72
  cli.command("create", "Create a new feature flag", (yargs) => {
61
73
  return yargs
@@ -78,6 +90,8 @@ cli.command("create", "Create a new feature flag", (yargs) => {
78
90
  input.groups = argv.groups;
79
91
  if (argv.users !== undefined)
80
92
  input.users = argv.users;
93
+ if (argv.tags !== undefined)
94
+ input.tags = argv.tags;
81
95
  if (argv.note !== undefined)
82
96
  input.note = argv.note;
83
97
  if (argv.expires !== undefined)
@@ -95,15 +109,19 @@ cli.command("create", "Create a new feature flag", (yargs) => {
95
109
  });
96
110
  // Get by ID command
97
111
  cli.command("get <id>", "Get a feature flag by ID", (yargs) => {
98
- return yargs.positional("id", {
112
+ return yargs
113
+ .positional("id", {
99
114
  type: "number",
100
115
  describe: "Feature flag ID",
101
- });
116
+ })
117
+ .options(includeArchivedOption);
102
118
  }, async (argv) => {
103
119
  const db = createDatabase();
104
120
  const model = new FeatureFlagModel(db);
105
121
  try {
106
- const flag = await model.getById(argv.id);
122
+ const flag = await model.getById(argv.id, {
123
+ includeArchived: argv.includeArchived,
124
+ });
107
125
  if (flag) {
108
126
  console.log(JSON.stringify(flag, null, 2));
109
127
  }
@@ -122,15 +140,19 @@ cli.command("get <id>", "Get a feature flag by ID", (yargs) => {
122
140
  });
123
141
  // Get by name command
124
142
  cli.command("get-by-name <name>", "Get a feature flag by name", (yargs) => {
125
- return yargs.positional("name", {
143
+ return yargs
144
+ .positional("name", {
126
145
  type: "string",
127
146
  describe: "Feature flag name",
128
- });
147
+ })
148
+ .options(includeArchivedOption);
129
149
  }, async (argv) => {
130
150
  const db = createDatabase();
131
151
  const model = new FeatureFlagModel(db);
132
152
  try {
133
- const flag = await model.getByName(argv.name);
153
+ const flag = await model.getByName(argv.name, {
154
+ includeArchived: argv.includeArchived,
155
+ });
134
156
  if (flag) {
135
157
  console.log(JSON.stringify(flag, null, 2));
136
158
  }
@@ -148,11 +170,15 @@ cli.command("get-by-name <name>", "Get a feature flag by name", (yargs) => {
148
170
  }
149
171
  });
150
172
  // List command
151
- cli.command("list", "List all feature flags", () => { }, async () => {
173
+ cli.command("list", "List all feature flags", (yargs) => {
174
+ return yargs.options(includeArchivedOption);
175
+ }, async (argv) => {
152
176
  const db = createDatabase();
153
177
  const model = new FeatureFlagModel(db);
154
178
  try {
155
- const flags = await model.list();
179
+ const flags = await model.list({
180
+ includeArchived: argv.includeArchived,
181
+ });
156
182
  console.log(JSON.stringify(flags, null, 2));
157
183
  }
158
184
  catch (error) {
@@ -163,6 +189,31 @@ cli.command("list", "List all feature flags", () => { }, async () => {
163
189
  await db.end();
164
190
  }
165
191
  });
192
+ // List flags carrying a given tag
193
+ cli.command("list-by-tag <tag>", "List feature flags carrying a given tag (e.g. release:1.5.0)", (yargs) => {
194
+ return yargs
195
+ .positional("tag", {
196
+ type: "string",
197
+ describe: "Tag to filter by",
198
+ })
199
+ .options(includeArchivedOption);
200
+ }, async (argv) => {
201
+ const db = createDatabase();
202
+ const model = new FeatureFlagModel(db);
203
+ try {
204
+ const flags = await model.listByTag(argv.tag, {
205
+ includeArchived: argv.includeArchived,
206
+ });
207
+ console.log(JSON.stringify(flags, null, 2));
208
+ }
209
+ catch (error) {
210
+ console.error("Error listing feature flags by tag:", error);
211
+ process.exit(1);
212
+ }
213
+ finally {
214
+ await db.end();
215
+ }
216
+ });
166
217
  // Update command
167
218
  cli.command("update <id>", "Update a feature flag", (yargs) => {
168
219
  return yargs
@@ -192,6 +243,10 @@ cli.command("update <id>", "Update a feature flag", (yargs) => {
192
243
  type: "boolean",
193
244
  describe: "Clear users list",
194
245
  },
246
+ "clear-tags": {
247
+ type: "boolean",
248
+ describe: "Clear tags list",
249
+ },
195
250
  "clear-note": {
196
251
  type: "boolean",
197
252
  describe: "Clear the note",
@@ -233,6 +288,11 @@ cli.command("update <id>", "Update a feature flag", (yargs) => {
233
288
  changes.users = null;
234
289
  else if (argv.users !== undefined)
235
290
  changes.users = argv.users;
291
+ // Tags: clear flag takes precedence
292
+ if (argv.clearTags)
293
+ changes.tags = null;
294
+ else if (argv.tags !== undefined)
295
+ changes.tags = argv.tags;
236
296
  // Note: clear flag takes precedence
237
297
  if (argv.clearNote)
238
298
  changes.note = null;
@@ -287,6 +347,33 @@ cli.command("delete <id>", "Delete a feature flag", (yargs) => {
287
347
  await db.end();
288
348
  }
289
349
  });
350
+ // Archive command (soft delete; irreversible, hides the flag but keeps history)
351
+ cli.command("archive <id>", "Archive (hide) a feature flag while keeping its history. Irreversible.", (yargs) => {
352
+ return yargs.positional("id", {
353
+ type: "number",
354
+ describe: "Feature flag ID",
355
+ });
356
+ }, async (argv) => {
357
+ const db = createDatabase();
358
+ const model = new FeatureFlagModel(db);
359
+ try {
360
+ const flag = await model.archive(argv.id);
361
+ if (flag) {
362
+ console.log(JSON.stringify(flag, null, 2));
363
+ }
364
+ else {
365
+ console.log(`Feature flag with ID ${argv.id} not found or already archived`);
366
+ process.exit(1);
367
+ }
368
+ }
369
+ catch (error) {
370
+ console.error("Error archiving feature flag:", error);
371
+ process.exit(1);
372
+ }
373
+ finally {
374
+ await db.end();
375
+ }
376
+ });
290
377
  // Check if active for user command
291
378
  cli.command("is-active <name>", "Check if a feature flag is active for a user", (yargs) => {
292
379
  return yargs
@@ -334,6 +421,777 @@ cli.command("is-active <name>", "Check if a feature flag is active for a user",
334
421
  await db.end();
335
422
  }
336
423
  });
424
+ // Check multiple flags for user command
425
+ cli.command("are-active", "Check if multiple feature flags are active for a user", (yargs) => {
426
+ return yargs.options({
427
+ names: {
428
+ type: "array",
429
+ describe: "Optional list of feature flag names (defaults to all flags)",
430
+ },
431
+ user: {
432
+ type: "string",
433
+ describe: "User ID",
434
+ },
435
+ roles: {
436
+ type: "array",
437
+ describe: "User roles",
438
+ },
439
+ groups: {
440
+ type: "array",
441
+ describe: "User groups",
442
+ },
443
+ });
444
+ }, async (argv) => {
445
+ const db = createDatabase();
446
+ const model = new FeatureFlagModel(db);
447
+ try {
448
+ const results = await model.areActiveForUser({
449
+ names: argv.names,
450
+ user: argv.user,
451
+ roles: argv.roles,
452
+ groups: argv.groups,
453
+ });
454
+ console.log(JSON.stringify({
455
+ names: argv.names,
456
+ user: argv.user,
457
+ roles: argv.roles,
458
+ groups: argv.groups,
459
+ results,
460
+ }, null, 2));
461
+ }
462
+ catch (error) {
463
+ console.error("Error checking multiple feature flags:", error);
464
+ process.exit(1);
465
+ }
466
+ finally {
467
+ await db.end();
468
+ }
469
+ });
470
+ // Check if active for context command
471
+ cli.command("is-active-context <name>", "Check if a feature flag is active for a context including subjects", (yargs) => {
472
+ return yargs
473
+ .positional("name", {
474
+ type: "string",
475
+ describe: "Feature flag name",
476
+ })
477
+ .options({
478
+ user: {
479
+ type: "string",
480
+ describe: "User ID",
481
+ },
482
+ roles: {
483
+ type: "array",
484
+ describe: "User roles",
485
+ },
486
+ groups: {
487
+ type: "array",
488
+ describe: "User groups",
489
+ },
490
+ subjects: {
491
+ type: "array",
492
+ describe: "External subject IDs (for example tenant:acme)",
493
+ },
494
+ });
495
+ }, async (argv) => {
496
+ const db = createDatabase();
497
+ const model = new FeatureFlagModel(db);
498
+ try {
499
+ const isActive = await model.isActiveForContext({
500
+ name: argv.name,
501
+ user: argv.user,
502
+ roles: argv.roles,
503
+ groups: argv.groups,
504
+ subjects: argv.subjects,
505
+ });
506
+ console.log(JSON.stringify({
507
+ name: argv.name,
508
+ isActive,
509
+ user: argv.user,
510
+ roles: argv.roles,
511
+ groups: argv.groups,
512
+ subjects: argv.subjects,
513
+ }, null, 2));
514
+ }
515
+ catch (error) {
516
+ console.error("Error checking feature flag context:", error);
517
+ process.exit(1);
518
+ }
519
+ finally {
520
+ await db.end();
521
+ }
522
+ });
523
+ // Add a subject to a feature flag
524
+ cli.command("add-subject <id> <subject>", "Add an external subject ID to a feature flag", (yargs) => {
525
+ return yargs
526
+ .positional("id", {
527
+ type: "number",
528
+ describe: "Feature flag ID",
529
+ })
530
+ .positional("subject", {
531
+ type: "string",
532
+ describe: "External subject ID (for example tenant:acme)",
533
+ });
534
+ }, async (argv) => {
535
+ const db = createDatabase();
536
+ const model = new FeatureFlagModel(db);
537
+ try {
538
+ const added = await model.addSubject(argv.id, argv.subject);
539
+ console.log(JSON.stringify({
540
+ id: argv.id,
541
+ subject: argv.subject,
542
+ added,
543
+ }, null, 2));
544
+ }
545
+ catch (error) {
546
+ console.error("Error adding subject:", error);
547
+ process.exit(1);
548
+ }
549
+ finally {
550
+ await db.end();
551
+ }
552
+ });
553
+ // Remove a subject from a feature flag
554
+ cli.command("remove-subject <id> <subject>", "Remove an external subject ID from a feature flag", (yargs) => {
555
+ return yargs
556
+ .positional("id", {
557
+ type: "number",
558
+ describe: "Feature flag ID",
559
+ })
560
+ .positional("subject", {
561
+ type: "string",
562
+ describe: "External subject ID",
563
+ });
564
+ }, async (argv) => {
565
+ const db = createDatabase();
566
+ const model = new FeatureFlagModel(db);
567
+ try {
568
+ const removed = await model.removeSubject(argv.id, argv.subject);
569
+ console.log(JSON.stringify({
570
+ id: argv.id,
571
+ subject: argv.subject,
572
+ removed,
573
+ }, null, 2));
574
+ }
575
+ catch (error) {
576
+ console.error("Error removing subject:", error);
577
+ process.exit(1);
578
+ }
579
+ finally {
580
+ await db.end();
581
+ }
582
+ });
583
+ // List subjects for a feature flag
584
+ cli.command("list-subjects <id>", "List external subject IDs for a feature flag", (yargs) => {
585
+ return yargs.positional("id", {
586
+ type: "number",
587
+ describe: "Feature flag ID",
588
+ });
589
+ }, async (argv) => {
590
+ const db = createDatabase();
591
+ const model = new FeatureFlagModel(db);
592
+ try {
593
+ const subjects = await model.getSubjects(argv.id);
594
+ console.log(JSON.stringify({
595
+ id: argv.id,
596
+ subjects,
597
+ }, null, 2));
598
+ }
599
+ catch (error) {
600
+ console.error("Error listing subjects:", error);
601
+ process.exit(1);
602
+ }
603
+ finally {
604
+ await db.end();
605
+ }
606
+ });
607
+ // Check multiple flags for context command
608
+ cli.command("are-active-context", "Check if multiple feature flags are active for a context including subjects", (yargs) => {
609
+ return yargs.options({
610
+ names: {
611
+ type: "array",
612
+ describe: "Optional list of feature flag names (defaults to all flags)",
613
+ },
614
+ user: {
615
+ type: "string",
616
+ describe: "User ID",
617
+ },
618
+ roles: {
619
+ type: "array",
620
+ describe: "User roles",
621
+ },
622
+ groups: {
623
+ type: "array",
624
+ describe: "User groups",
625
+ },
626
+ subjects: {
627
+ type: "array",
628
+ describe: "External subject IDs (for example tenant:acme)",
629
+ },
630
+ });
631
+ }, async (argv) => {
632
+ const db = createDatabase();
633
+ const model = new FeatureFlagModel(db);
634
+ try {
635
+ const results = await model.areActiveForContext({
636
+ names: argv.names,
637
+ user: argv.user,
638
+ roles: argv.roles,
639
+ groups: argv.groups,
640
+ subjects: argv.subjects,
641
+ });
642
+ console.log(JSON.stringify({
643
+ names: argv.names,
644
+ user: argv.user,
645
+ roles: argv.roles,
646
+ groups: argv.groups,
647
+ subjects: argv.subjects,
648
+ results,
649
+ }, null, 2));
650
+ }
651
+ catch (error) {
652
+ console.error("Error checking multiple feature flag contexts:", error);
653
+ process.exit(1);
654
+ }
655
+ finally {
656
+ await db.end();
657
+ }
658
+ });
659
+ // List flags that directly match a subject
660
+ cli.command("list-by-subject <subject>", "List feature flags directly assigned to an external subject ID", (yargs) => {
661
+ return yargs.positional("subject", {
662
+ type: "string",
663
+ describe: "External subject ID",
664
+ });
665
+ }, async (argv) => {
666
+ const db = createDatabase();
667
+ const model = new FeatureFlagModel(db);
668
+ try {
669
+ const flags = await model.getFeatureFlagsForSubject(argv.subject);
670
+ console.log(JSON.stringify({
671
+ subject: argv.subject,
672
+ flags,
673
+ }, null, 2));
674
+ }
675
+ catch (error) {
676
+ console.error("Error listing flags by subject:", error);
677
+ process.exit(1);
678
+ }
679
+ finally {
680
+ await db.end();
681
+ }
682
+ });
683
+ // Create feature flag group command
684
+ cli.command("group-create", "Create a feature flag group", (yargs) => {
685
+ return yargs
686
+ .options({
687
+ name: {
688
+ type: "string",
689
+ describe: "Feature flag group name",
690
+ },
691
+ note: {
692
+ type: "string",
693
+ describe: "Feature flag group description",
694
+ },
695
+ })
696
+ .demandOption("name", "Feature flag group name is required");
697
+ }, async (argv) => {
698
+ const db = createDatabase();
699
+ const model = new FeatureFlagGroupModel(db);
700
+ try {
701
+ const group = await model.create({
702
+ name: argv.name,
703
+ note: argv.note,
704
+ });
705
+ console.log(JSON.stringify(group, null, 2));
706
+ }
707
+ catch (error) {
708
+ console.error("Error creating feature flag group:", error);
709
+ process.exit(1);
710
+ }
711
+ finally {
712
+ await db.end();
713
+ }
714
+ });
715
+ // List feature flag groups command
716
+ cli.command("group-list", "List all feature flag groups", (yargs) => {
717
+ return yargs.options(includeArchivedOption);
718
+ }, async (argv) => {
719
+ const db = createDatabase();
720
+ const model = new FeatureFlagGroupModel(db);
721
+ try {
722
+ const groups = await model.list({
723
+ includeArchived: argv.includeArchived,
724
+ });
725
+ console.log(JSON.stringify(groups, null, 2));
726
+ }
727
+ catch (error) {
728
+ console.error("Error listing feature flag groups:", error);
729
+ process.exit(1);
730
+ }
731
+ finally {
732
+ await db.end();
733
+ }
734
+ });
735
+ // Get feature flag group by ID command
736
+ cli.command("group-get <id>", "Get a feature flag group by ID", (yargs) => {
737
+ return yargs
738
+ .positional("id", {
739
+ type: "number",
740
+ describe: "Feature flag group ID",
741
+ })
742
+ .options(includeArchivedOption);
743
+ }, async (argv) => {
744
+ const db = createDatabase();
745
+ const model = new FeatureFlagGroupModel(db);
746
+ try {
747
+ const group = await model.getById(argv.id, {
748
+ includeArchived: argv.includeArchived,
749
+ });
750
+ if (!group) {
751
+ console.log(`Feature flag group with ID ${argv.id} not found`);
752
+ process.exit(1);
753
+ }
754
+ console.log(JSON.stringify(group, null, 2));
755
+ }
756
+ catch (error) {
757
+ console.error("Error getting feature flag group:", error);
758
+ process.exit(1);
759
+ }
760
+ finally {
761
+ await db.end();
762
+ }
763
+ });
764
+ // Get feature flag group by name command
765
+ cli.command("group-get-by-name <name>", "Get a feature flag group by name", (yargs) => {
766
+ return yargs
767
+ .positional("name", {
768
+ type: "string",
769
+ describe: "Feature flag group name",
770
+ })
771
+ .options(includeArchivedOption);
772
+ }, async (argv) => {
773
+ const db = createDatabase();
774
+ const model = new FeatureFlagGroupModel(db);
775
+ try {
776
+ const group = await model.getByName(argv.name, {
777
+ includeArchived: argv.includeArchived,
778
+ });
779
+ if (!group) {
780
+ console.log(`Feature flag group with name "${argv.name}" not found`);
781
+ process.exit(1);
782
+ }
783
+ console.log(JSON.stringify(group, null, 2));
784
+ }
785
+ catch (error) {
786
+ console.error("Error getting feature flag group by name:", error);
787
+ process.exit(1);
788
+ }
789
+ finally {
790
+ await db.end();
791
+ }
792
+ });
793
+ // Update feature flag group command
794
+ cli.command("group-update <id>", "Update a feature flag group", (yargs) => {
795
+ return yargs
796
+ .positional("id", {
797
+ type: "number",
798
+ describe: "Feature flag group ID",
799
+ })
800
+ .options({
801
+ name: {
802
+ type: "string",
803
+ describe: "Feature flag group name",
804
+ },
805
+ note: {
806
+ type: "string",
807
+ describe: "Feature flag group description",
808
+ },
809
+ "clear-note": {
810
+ type: "boolean",
811
+ describe: "Clear the group note",
812
+ },
813
+ });
814
+ }, async (argv) => {
815
+ const db = createDatabase();
816
+ const model = new FeatureFlagGroupModel(db);
817
+ try {
818
+ const changes = {};
819
+ if (argv.name !== undefined)
820
+ changes.name = argv.name;
821
+ if (argv.clearNote)
822
+ changes.note = null;
823
+ else if (argv.note !== undefined)
824
+ changes.note = argv.note;
825
+ const group = await model.update(argv.id, changes);
826
+ if (!group) {
827
+ console.log(`Feature flag group with ID ${argv.id} not found`);
828
+ process.exit(1);
829
+ }
830
+ console.log(JSON.stringify(group, null, 2));
831
+ }
832
+ catch (error) {
833
+ console.error("Error updating feature flag group:", error);
834
+ process.exit(1);
835
+ }
836
+ finally {
837
+ await db.end();
838
+ }
839
+ });
840
+ // Delete feature flag group command
841
+ cli.command("group-delete <id>", "Delete a feature flag group", (yargs) => {
842
+ return yargs.positional("id", {
843
+ type: "number",
844
+ describe: "Feature flag group ID",
845
+ });
846
+ }, async (argv) => {
847
+ const db = createDatabase();
848
+ const model = new FeatureFlagGroupModel(db);
849
+ try {
850
+ const deleted = await model.delete(argv.id);
851
+ if (!deleted) {
852
+ console.log(`Feature flag group with ID ${argv.id} not found`);
853
+ process.exit(1);
854
+ }
855
+ console.log(`Feature flag group with ID ${argv.id} deleted successfully`);
856
+ }
857
+ catch (error) {
858
+ console.error("Error deleting feature flag group:", error);
859
+ process.exit(1);
860
+ }
861
+ finally {
862
+ await db.end();
863
+ }
864
+ });
865
+ // Archive a feature flag group (soft delete; irreversible, keeps history)
866
+ cli.command("group-archive <id>", "Archive (hide) a feature flag group while keeping its history. Irreversible.", (yargs) => {
867
+ return yargs.positional("id", {
868
+ type: "number",
869
+ describe: "Feature flag group ID",
870
+ });
871
+ }, async (argv) => {
872
+ const db = createDatabase();
873
+ const model = new FeatureFlagGroupModel(db);
874
+ try {
875
+ const group = await model.archive(argv.id);
876
+ if (!group) {
877
+ console.log(`Feature flag group with ID ${argv.id} not found or already archived`);
878
+ process.exit(1);
879
+ }
880
+ console.log(JSON.stringify(group, null, 2));
881
+ }
882
+ catch (error) {
883
+ console.error("Error archiving feature flag group:", error);
884
+ process.exit(1);
885
+ }
886
+ finally {
887
+ await db.end();
888
+ }
889
+ });
890
+ // Add flag to feature flag group command
891
+ cli.command("group-add-flag <groupId> <flagId>", "Add a feature flag to a group", (yargs) => {
892
+ return yargs
893
+ .positional("groupId", {
894
+ type: "number",
895
+ describe: "Feature flag group ID",
896
+ })
897
+ .positional("flagId", {
898
+ type: "number",
899
+ describe: "Feature flag ID",
900
+ });
901
+ }, async (argv) => {
902
+ const db = createDatabase();
903
+ const model = new FeatureFlagGroupModel(db);
904
+ try {
905
+ const added = await model.addFeatureFlag(argv.groupId, argv.flagId);
906
+ console.log(JSON.stringify({
907
+ groupId: argv.groupId,
908
+ flagId: argv.flagId,
909
+ added,
910
+ }, null, 2));
911
+ }
912
+ catch (error) {
913
+ console.error("Error adding feature flag to group:", error);
914
+ process.exit(1);
915
+ }
916
+ finally {
917
+ await db.end();
918
+ }
919
+ });
920
+ // Remove flag from feature flag group command
921
+ cli.command("group-remove-flag <groupId> <flagId>", "Remove a feature flag from a group", (yargs) => {
922
+ return yargs
923
+ .positional("groupId", {
924
+ type: "number",
925
+ describe: "Feature flag group ID",
926
+ })
927
+ .positional("flagId", {
928
+ type: "number",
929
+ describe: "Feature flag ID",
930
+ });
931
+ }, async (argv) => {
932
+ const db = createDatabase();
933
+ const model = new FeatureFlagGroupModel(db);
934
+ try {
935
+ const removed = await model.removeFeatureFlag(argv.groupId, argv.flagId);
936
+ console.log(JSON.stringify({
937
+ groupId: argv.groupId,
938
+ flagId: argv.flagId,
939
+ removed,
940
+ }, null, 2));
941
+ }
942
+ catch (error) {
943
+ console.error("Error removing feature flag from group:", error);
944
+ process.exit(1);
945
+ }
946
+ finally {
947
+ await db.end();
948
+ }
949
+ });
950
+ // List flags in feature flag group command
951
+ cli.command("group-list-flags <groupId>", "List all feature flags in a group", (yargs) => {
952
+ return yargs.positional("groupId", {
953
+ type: "number",
954
+ describe: "Feature flag group ID",
955
+ });
956
+ }, async (argv) => {
957
+ const db = createDatabase();
958
+ const model = new FeatureFlagGroupModel(db);
959
+ try {
960
+ const flags = await model.getFeatureFlags(argv.groupId);
961
+ console.log(JSON.stringify({
962
+ groupId: argv.groupId,
963
+ flags,
964
+ }, null, 2));
965
+ }
966
+ catch (error) {
967
+ console.error("Error listing flags for group:", error);
968
+ process.exit(1);
969
+ }
970
+ finally {
971
+ await db.end();
972
+ }
973
+ });
974
+ // List groups for feature flag command
975
+ cli.command("group-list-for-flag <flagId>", "List all groups that contain a feature flag", (yargs) => {
976
+ return yargs.positional("flagId", {
977
+ type: "number",
978
+ describe: "Feature flag ID",
979
+ });
980
+ }, async (argv) => {
981
+ const db = createDatabase();
982
+ const model = new FeatureFlagGroupModel(db);
983
+ try {
984
+ const groups = await model.getGroupsForFeatureFlag(argv.flagId);
985
+ console.log(JSON.stringify({
986
+ flagId: argv.flagId,
987
+ groups,
988
+ }, null, 2));
989
+ }
990
+ catch (error) {
991
+ console.error("Error listing groups for feature flag:", error);
992
+ process.exit(1);
993
+ }
994
+ finally {
995
+ await db.end();
996
+ }
997
+ });
998
+ // Bulk update flags in a group command
999
+ cli.command("group-update-all <groupId>", "Update all feature flags in a group", (yargs) => {
1000
+ return yargs
1001
+ .positional("groupId", {
1002
+ type: "number",
1003
+ describe: "Feature flag group ID",
1004
+ })
1005
+ .options({
1006
+ everyone: {
1007
+ type: "boolean",
1008
+ describe: "Enable flag for everyone (overrides all other settings)",
1009
+ },
1010
+ percent: {
1011
+ type: "number",
1012
+ describe: "Percentage rollout (0-99.9)",
1013
+ },
1014
+ roles: {
1015
+ type: "array",
1016
+ describe: "List of roles that have this flag enabled",
1017
+ },
1018
+ groups: {
1019
+ type: "array",
1020
+ describe: "List of user groups that have this flag enabled",
1021
+ },
1022
+ users: {
1023
+ type: "array",
1024
+ describe: "List of specific user IDs that have this flag enabled",
1025
+ },
1026
+ "clear-everyone": {
1027
+ type: "boolean",
1028
+ describe: "Unset the everyone override",
1029
+ },
1030
+ "clear-percent": {
1031
+ type: "boolean",
1032
+ describe: "Unset percentage rollout",
1033
+ },
1034
+ "clear-roles": {
1035
+ type: "boolean",
1036
+ describe: "Clear roles list",
1037
+ },
1038
+ "clear-groups": {
1039
+ type: "boolean",
1040
+ describe: "Clear groups list",
1041
+ },
1042
+ "clear-users": {
1043
+ type: "boolean",
1044
+ describe: "Clear users list",
1045
+ },
1046
+ });
1047
+ }, async (argv) => {
1048
+ const db = createDatabase();
1049
+ const model = new FeatureFlagGroupModel(db);
1050
+ try {
1051
+ const changes = {};
1052
+ if (argv.clearEveryone)
1053
+ changes.everyone = null;
1054
+ else if (argv.everyone !== undefined)
1055
+ changes.everyone = argv.everyone;
1056
+ if (argv.clearPercent)
1057
+ changes.percent = null;
1058
+ else if (argv.percent !== undefined)
1059
+ changes.percent = argv.percent;
1060
+ if (argv.clearRoles)
1061
+ changes.roles = null;
1062
+ else if (argv.roles !== undefined)
1063
+ changes.roles = argv.roles;
1064
+ if (argv.clearGroups)
1065
+ changes.groups = null;
1066
+ else if (argv.groups !== undefined)
1067
+ changes.groups = argv.groups;
1068
+ if (argv.clearUsers)
1069
+ changes.users = null;
1070
+ else if (argv.users !== undefined)
1071
+ changes.users = argv.users;
1072
+ const updatedCount = await model.updateAll(argv.groupId, changes);
1073
+ console.log(JSON.stringify({
1074
+ groupId: argv.groupId,
1075
+ updatedCount,
1076
+ changes,
1077
+ }, null, 2));
1078
+ }
1079
+ catch (error) {
1080
+ console.error("Error updating all flags in group:", error);
1081
+ process.exit(1);
1082
+ }
1083
+ finally {
1084
+ await db.end();
1085
+ }
1086
+ });
1087
+ // Add subject to feature flag group command
1088
+ cli.command("group-add-subject <groupId> <subject>", "Add an external subject ID to a feature flag group", (yargs) => {
1089
+ return yargs
1090
+ .positional("groupId", {
1091
+ type: "number",
1092
+ describe: "Feature flag group ID",
1093
+ })
1094
+ .positional("subject", {
1095
+ type: "string",
1096
+ describe: "External subject ID",
1097
+ });
1098
+ }, async (argv) => {
1099
+ const db = createDatabase();
1100
+ const model = new FeatureFlagGroupModel(db);
1101
+ try {
1102
+ const added = await model.addSubject(argv.groupId, argv.subject);
1103
+ console.log(JSON.stringify({
1104
+ groupId: argv.groupId,
1105
+ subject: argv.subject,
1106
+ added,
1107
+ }, null, 2));
1108
+ }
1109
+ catch (error) {
1110
+ console.error("Error adding subject to group:", error);
1111
+ process.exit(1);
1112
+ }
1113
+ finally {
1114
+ await db.end();
1115
+ }
1116
+ });
1117
+ // Remove subject from feature flag group command
1118
+ cli.command("group-remove-subject <groupId> <subject>", "Remove an external subject ID from a feature flag group", (yargs) => {
1119
+ return yargs
1120
+ .positional("groupId", {
1121
+ type: "number",
1122
+ describe: "Feature flag group ID",
1123
+ })
1124
+ .positional("subject", {
1125
+ type: "string",
1126
+ describe: "External subject ID",
1127
+ });
1128
+ }, async (argv) => {
1129
+ const db = createDatabase();
1130
+ const model = new FeatureFlagGroupModel(db);
1131
+ try {
1132
+ const removed = await model.removeSubject(argv.groupId, argv.subject);
1133
+ console.log(JSON.stringify({
1134
+ groupId: argv.groupId,
1135
+ subject: argv.subject,
1136
+ removed,
1137
+ }, null, 2));
1138
+ }
1139
+ catch (error) {
1140
+ console.error("Error removing subject from group:", error);
1141
+ process.exit(1);
1142
+ }
1143
+ finally {
1144
+ await db.end();
1145
+ }
1146
+ });
1147
+ // List subjects for feature flag group command
1148
+ cli.command("group-list-subjects <groupId>", "List external subject IDs for a feature flag group", (yargs) => {
1149
+ return yargs.positional("groupId", {
1150
+ type: "number",
1151
+ describe: "Feature flag group ID",
1152
+ });
1153
+ }, async (argv) => {
1154
+ const db = createDatabase();
1155
+ const model = new FeatureFlagGroupModel(db);
1156
+ try {
1157
+ const subjects = await model.getSubjects(argv.groupId);
1158
+ console.log(JSON.stringify({
1159
+ groupId: argv.groupId,
1160
+ subjects,
1161
+ }, null, 2));
1162
+ }
1163
+ catch (error) {
1164
+ console.error("Error listing subjects for group:", error);
1165
+ process.exit(1);
1166
+ }
1167
+ finally {
1168
+ await db.end();
1169
+ }
1170
+ });
1171
+ // List groups for subject command
1172
+ cli.command("group-list-by-subject <subject>", "List feature flag groups assigned to an external subject ID", (yargs) => {
1173
+ return yargs.positional("subject", {
1174
+ type: "string",
1175
+ describe: "External subject ID",
1176
+ });
1177
+ }, async (argv) => {
1178
+ const db = createDatabase();
1179
+ const model = new FeatureFlagGroupModel(db);
1180
+ try {
1181
+ const groups = await model.getGroupsForSubject(argv.subject);
1182
+ console.log(JSON.stringify({
1183
+ subject: argv.subject,
1184
+ groups,
1185
+ }, null, 2));
1186
+ }
1187
+ catch (error) {
1188
+ console.error("Error listing groups by subject:", error);
1189
+ process.exit(1);
1190
+ }
1191
+ finally {
1192
+ await db.end();
1193
+ }
1194
+ });
337
1195
  // Hash user ID command (utility)
338
1196
  cli.command("hash-user <userId>", "Hash a user ID using the same algorithm as percentage rollout", (yargs) => {
339
1197
  return yargs.positional("userId", {