featurevisor 0.2.0 → 1.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 984dcbada7b55c304a639cf58935c7b1dd561db2d6159b178a3829709473925a
4
- data.tar.gz: 2a4068e17a151e671b45befb743be0935b19432e91411a243e2b3f8cbfa2c1e2
3
+ metadata.gz: 5951351cf7893e83e9f3994999f48389e1998bb8dec84092f3e7627f986bfd02
4
+ data.tar.gz: 3d24e02b8fd0708084b7716863396119728bcd83cbd0ac209b7a1c62fb982f68
5
5
  SHA512:
6
- metadata.gz: b813718a6962bd78a1142e5cd7758c3a0ea176d6fc8943114bfb6c630ece7e2dc160281ec7ca661d9de012366ba7b1e3ceeff9af6f6e7bfcdc4b53d89e3ac04b
7
- data.tar.gz: 3fcc3e23b3b773c7780e5d018679c74a453e263797342b0912b02c74d634d60b4029c7e042f74225ba6550d7f293ab9819ff657f51a318f90a026b145fc896bb
6
+ metadata.gz: 368e45cb26cb6a98d13bc14b4fe9116bd36af47acadad3fbc99fc1926c8ce768edf5c9ade31aa36dd3c85ff95fc6dab2d3433d4d607bbeee99076a21838e0dae
7
+ data.tar.gz: 160b9eb960915113e3f9dfa3507b9f06664224ac3517d7a84432ec64d2d26d26c2ba379a09aeba64efe78a310972b5f497ad5d5ef1bbef1cc41ffa1408ffa70f
data/README.md CHANGED
@@ -1,12 +1,13 @@
1
1
  # Featurevisor Ruby SDK <!-- omit in toc -->
2
2
 
3
- This is a port of Featurevisor [JavaScript SDK](https://featurevisor.com/docs/sdks/javascript/) v2.x to Ruby, providing a way to evaluate feature flags, variations, and variables in your Ruby applications.
3
+ This is a port of Featurevisor [JavaScript SDK](https://featurevisor.com/docs/sdks/javascript/) v3.x to Ruby, providing a way to evaluate feature flags, variations, and variables in your Ruby applications.
4
4
 
5
- This SDK is compatible with [Featurevisor](https://featurevisor.com/) v2.0 projects and above.
5
+ This SDK is compatible with Featurevisor v3 projects and v2 datafiles.
6
6
 
7
7
  ## Table of contents <!-- omit in toc -->
8
8
 
9
9
  - [Installation](#installation)
10
+ - [Public API](#public-api)
10
11
  - [Initialization](#initialization)
11
12
  - [Evaluation types](#evaluation-types)
12
13
  - [Context](#context)
@@ -23,24 +24,28 @@ This SDK is compatible with [Featurevisor](https://featurevisor.com/) v2.0 proje
23
24
  - [Initialize with sticky](#initialize-with-sticky)
24
25
  - [Set sticky afterwards](#set-sticky-afterwards)
25
26
  - [Setting datafile](#setting-datafile)
27
+ - [Merging by default](#merging-by-default)
28
+ - [Replacing](#replacing)
29
+ - [Loading datafiles on demand](#loading-datafiles-on-demand)
26
30
  - [Updating datafile](#updating-datafile)
27
31
  - [Interval-based update](#interval-based-update)
28
- - [Logging](#logging)
32
+ - [Evaluation details](#evaluation-details)
33
+ - [Diagnostics](#diagnostics)
29
34
  - [Levels](#levels)
30
- - [Customizing levels](#customizing-levels)
31
35
  - [Handler](#handler)
32
36
  - [Events](#events)
33
37
  - [`datafile_set`](#datafile_set)
34
38
  - [`context_set`](#context_set)
35
39
  - [`sticky_set`](#sticky_set)
36
- - [Evaluation details](#evaluation-details)
37
- - [Hooks](#hooks)
38
- - [Defining a hook](#defining-a-hook)
39
- - [Registering hooks](#registering-hooks)
40
+ - [`error`](#error)
41
+ - [Modules](#modules)
42
+ - [Defining a module](#defining-a-module)
43
+ - [Registering modules](#registering-modules)
40
44
  - [Child instance](#child-instance)
41
45
  - [Close](#close)
42
46
  - [CLI usage](#cli-usage)
43
47
  - [Test](#test)
48
+ - [Test against local monorepo's example-1](#test-against-local-monorepos-example-1)
44
49
  - [Benchmark](#benchmark)
45
50
  - [Assess distribution](#assess-distribution)
46
51
  - [Development](#development)
@@ -49,6 +54,8 @@ This SDK is compatible with [Featurevisor](https://featurevisor.com/) v2.0 proje
49
54
  - [Releasing](#releasing)
50
55
  - [License](#license)
51
56
 
57
+ <!-- FEATUREVISOR_DOCS_BEGIN -->
58
+
52
59
  ## Installation
53
60
 
54
61
  Add this line to your application's Gemfile:
@@ -69,6 +76,18 @@ Or install it yourself as:
69
76
  $ gem install featurevisor
70
77
  ```
71
78
 
79
+ ## Public API
80
+
81
+ The main runtime API is `Featurevisor.create_featurevisor`:
82
+
83
+ ```ruby
84
+ f = Featurevisor.create_featurevisor(
85
+ datafile: datafile_content
86
+ )
87
+ ```
88
+
89
+ Most applications only need this factory and the returned `Featurevisor::Instance`. Public extension and observability APIs include modules, diagnostics, events, and the datafile structures accepted by the factory.
90
+
72
91
  ## Initialization
73
92
 
74
93
  The SDK can be initialized by passing [datafile](https://featurevisor.com/docs/building-datafiles/) content directly:
@@ -86,7 +105,7 @@ response = Net::HTTP.get_response(URI(datafile_url))
86
105
  datafile_content = JSON.parse(response.body, symbolize_names: true)
87
106
 
88
107
  # Create SDK instance
89
- f = Featurevisor.create_instance(
108
+ f = Featurevisor.create_featurevisor(
90
109
  datafile: datafile_content
91
110
  )
92
111
  ```
@@ -98,10 +117,10 @@ Alternatively, you can pass a JSON string directly and the SDK will parse it aut
98
117
  ```ruby
99
118
  # Option 1: Parse JSON yourself (recommended)
100
119
  datafile_content = JSON.parse(json_string, symbolize_names: true)
101
- f = Featurevisor.create_instance(datafile: datafile_content)
120
+ f = Featurevisor.create_featurevisor(datafile: datafile_content)
102
121
 
103
122
  # Option 2: Pass JSON string directly (automatic parsing)
104
- f = Featurevisor.create_instance(datafile: json_string)
123
+ f = Featurevisor.create_featurevisor(datafile: json_string)
105
124
  ```
106
125
 
107
126
  ## Evaluation types
@@ -139,7 +158,7 @@ You can set context at the time of initialization:
139
158
  ```ruby
140
159
  require 'featurevisor'
141
160
 
142
- f = Featurevisor.create_instance(
161
+ f = Featurevisor.create_featurevisor(
143
162
  context: {
144
163
  deviceId: '123',
145
164
  country: 'nl'
@@ -271,6 +290,8 @@ f.get_variable_object(feature_key, variable_key, context = {})
271
290
  f.get_variable_json(feature_key, variable_key, context = {})
272
291
  ```
273
292
 
293
+ Type specific methods do not coerce values. `get_variable_integer` returns `nil` for the string `"1"`, and boolean getters return `nil` for non-boolean values.
294
+
274
295
  ## Getting all evaluations
275
296
 
276
297
  You can get evaluations of all features available in the SDK instance:
@@ -301,12 +322,14 @@ This is handy especially when you want to pass all evaluations from a backend ap
301
322
 
302
323
  For the lifecycle of the SDK instance in your application, you can set some features with sticky values, meaning that they will not be evaluated against the fetched [datafile](https://featurevisor.com/docs/building-datafiles/):
303
324
 
325
+ Sticky values belong to an SDK or child instance. Evaluation options do not accept sticky overrides; use `spawn(context, sticky: ...)` when a child needs its own sticky state.
326
+
304
327
  ### Initialize with sticky
305
328
 
306
329
  ```ruby
307
330
  require 'featurevisor'
308
331
 
309
- f = Featurevisor.create_instance(
332
+ f = Featurevisor.create_featurevisor(
310
333
  sticky: {
311
334
  myFeatureKey: {
312
335
  enabled: true,
@@ -359,6 +382,49 @@ f.set_datafile(json_string)
359
382
 
360
383
  **Important**: When calling `set_datafile()`, ensure JSON is parsed with `symbolize_names: true` if you're parsing it yourself.
361
384
 
385
+ ### Merging by default
386
+
387
+ By default, `set_datafile(datafile)` merges the incoming datafile into the SDK's current datafile:
388
+
389
+ - top-level metadata such as `schemaVersion`, `revision`, and `featurevisorVersion` comes from the incoming datafile
390
+ - `segments` are merged, with incoming entries overriding existing ones
391
+ - `features` are merged, with incoming entries overriding existing ones
392
+
393
+ This means you can call `set_datafile` more than once with different datafiles, and the SDK instance accumulates their features and segments together.
394
+
395
+ ### Replacing
396
+
397
+ To fully replace the stored datafile, pass `true` as the second argument:
398
+
399
+ ```ruby
400
+ f.set_datafile(datafile_content, true)
401
+ ```
402
+
403
+ ### Loading datafiles on demand
404
+
405
+ Because merging is the default, a single SDK instance can start with a small datafile and load more datafiles later as your application needs them, instead of downloading every feature upfront.
406
+
407
+ This pairs well with [targets](https://featurevisor.com/docs/targets/), where each target produces a smaller datafile for a specific part of your application:
408
+
409
+ ```ruby
410
+ require "open-uri"
411
+
412
+ f = Featurevisor.create_featurevisor({})
413
+
414
+ def load_datafile(f, target)
415
+ url = "https://cdn.yoursite.com/production/featurevisor-#{target}.json"
416
+ datafile = JSON.parse(URI.open(url).read, symbolize_names: true)
417
+
418
+ # merges into whatever was loaded before
419
+ f.set_datafile(datafile)
420
+ end
421
+
422
+ load_datafile(f, "products")
423
+
424
+ # later, when the user reaches checkout
425
+ load_datafile(f, "checkout")
426
+ ```
427
+
362
428
  ### Updating datafile
363
429
 
364
430
  You can set the datafile as many times as you want in your application, which will result in emitting a [`datafile_set`](#datafile_set) event that you can listen and react to accordingly.
@@ -381,7 +447,7 @@ require 'json'
381
447
  def update_datafile(f, datafile_url)
382
448
  loop do
383
449
  sleep(5 * 60) # 5 minutes
384
-
450
+
385
451
  begin
386
452
  response = Net::HTTP.get_response(URI(datafile_url))
387
453
  datafile_content = JSON.parse(response.body)
@@ -397,65 +463,38 @@ end
397
463
  Thread.new { update_datafile(f, datafile_url) }
398
464
  ```
399
465
 
400
- ## Logging
466
+ ## Diagnostics
401
467
 
402
- By default, Featurevisor SDKs will print out logs to the console for `info` level and above.
468
+ By default, Featurevisor reports diagnostics to the console for `info` level and above with a `[Featurevisor]` prefix.
403
469
 
404
470
  ### Levels
405
471
 
406
- These are all the available log levels:
407
-
408
- - `error`
409
- - `warn`
410
- - `info`
411
- - `debug`
412
-
413
- ### Customizing levels
414
-
415
- If you choose `debug` level to make the logs more verbose, you can set it at the time of SDK initialization.
416
-
417
- Setting `debug` level will print out all logs, including `info`, `warn`, and `error` levels.
418
-
419
- ```ruby
420
- require 'featurevisor'
421
-
422
- f = Featurevisor.create_instance(
423
- logger: Featurevisor.create_logger(level: 'debug')
424
- )
425
- ```
472
+ Available diagnostic levels are `fatal`, `error`, `warn`, `info`, and `debug`.
426
473
 
427
- Alternatively, you can also set `log_level` directly:
474
+ Set the level during initialization or update it afterwards:
428
475
 
429
476
  ```ruby
430
- f = Featurevisor.create_instance(
431
- log_level: 'debug'
432
- )
433
- ```
434
-
435
- You can also set log level from SDK instance afterwards:
436
-
437
- ```ruby
438
- f.set_log_level('debug')
477
+ f = Featurevisor.create_featurevisor(log_level: "debug")
478
+ f.set_log_level("info")
439
479
  ```
440
480
 
441
481
  ### Handler
442
482
 
443
- You can also pass your own log handler, if you do not wish to print the logs to the console:
483
+ Use `on_diagnostic` to send structured diagnostics to your observability system:
444
484
 
445
485
  ```ruby
446
- require 'featurevisor'
447
-
448
- f = Featurevisor.create_instance(
449
- logger: Featurevisor.create_logger(
450
- level: 'info',
451
- handler: ->(level, message, details) {
452
- # do something with the log
453
- }
454
- )
486
+ f = Featurevisor.create_featurevisor(
487
+ log_level: "info",
488
+ on_diagnostic: ->(diagnostic) {
489
+ puts "#{diagnostic[:level]} #{diagnostic[:code]} #{diagnostic[:message]}"
490
+ }
455
491
  )
456
492
  ```
457
493
 
458
- Further log levels like `info` and `debug` will help you understand how the feature variations and variables are evaluated in the runtime against given context.
494
+ Every diagnostic has `:level`, `:code`, `:message`, and an object-shaped `:details` hash. Optional `:module`, `:moduleName`, and `:originalError` fields describe provenance. Evaluation metadata belongs in `:details`.
495
+
496
+ Diagnostic handlers are isolated from SDK behavior. An exception in a handler does not stop other handlers or evaluations.
497
+
459
498
 
460
499
  ## Events
461
500
 
@@ -468,8 +507,8 @@ You can listen to these events that can occur at various stages in your applicat
468
507
  ```ruby
469
508
  unsubscribe = f.on('datafile_set') do |event|
470
509
  revision = event[:revision] # new revision
471
- previous_revision = event[:previous_revision]
472
- revision_changed = event[:revision_changed] # true if revision has changed
510
+ previous_revision = event[:previousRevision]
511
+ revision_changed = event[:revisionChanged] # true if revision has changed
473
512
 
474
513
  # list of feature keys that have new updates,
475
514
  # and you should re-evaluate them
@@ -490,6 +529,8 @@ The `features` array will contain keys of features that have either been:
490
529
 
491
530
  compared to the previous datafile content that existed in the SDK instance.
492
531
 
532
+ The event also includes `replaced`, which is `true` when the datafile replaced the previous content instead of merging into it.
533
+
493
534
  ### `context_set`
494
535
 
495
536
  ```ruby
@@ -512,6 +553,20 @@ unsubscribe = f.on('sticky_set') do |event|
512
553
  end
513
554
  ```
514
555
 
556
+ ### `error`
557
+
558
+ ```ruby
559
+ unsubscribe = f.on('error') do |event|
560
+ diagnostic = event[:diagnostic]
561
+ code = diagnostic[:code]
562
+ message = diagnostic[:message]
563
+
564
+ puts "Featurevisor error: #{code} #{message}"
565
+ end
566
+ ```
567
+
568
+ The `error` event is emitted for diagnostics reported with `level: "error"`.
569
+
515
570
  ## Evaluation details
516
571
 
517
572
  Besides logging with debug level enabled, you can also get more details about how the feature variations and variables are evaluated in the runtime against given context:
@@ -544,22 +599,33 @@ And optionally these properties depending on whether you are evaluating a featur
544
599
  - `variable_value`: the variable value
545
600
  - `variable_schema`: the variable schema
546
601
 
547
- ## Hooks
602
+ ## Modules
603
+
604
+ Modules allow you to intercept the evaluation process and customize SDK behavior.
548
605
 
549
- Hooks allow you to intercept the evaluation process and customize it further as per your needs.
606
+ ### Defining a module
550
607
 
551
- ### Defining a hook
608
+ A module is a simple hash with a unique recommended `name` and optional lifecycle functions:
552
609
 
553
- A hook is a simple hash with a unique required `name` and optional functions:
610
+ If `setup` raises an exception, the module is not registered. Featurevisor removes subscriptions created during setup, reports `module_setup_error`, and calls `close` when present.
554
611
 
555
612
  ```ruby
556
613
  require 'featurevisor'
557
614
 
558
- my_custom_hook = {
559
- # only required property
560
- name: 'my-custom-hook',
615
+ my_custom_module = {
616
+ # recommended, and used for duplicate detection/removal
617
+ name: 'my-custom-module',
618
+
619
+ # rest of the properties below are all optional per module
620
+
621
+ # setup once, when the module is registered
622
+ setup: ->(api) {
623
+ revision = api[:get_revision].call
561
624
 
562
- # rest of the properties below are all optional per hook
625
+ api[:on_diagnostic].call(->(diagnostic) {
626
+ puts diagnostic[:message]
627
+ })
628
+ },
563
629
 
564
630
  # before evaluation
565
631
  before: ->(options) {
@@ -589,26 +655,43 @@ my_custom_hook = {
589
655
  bucket_value: ->(options) {
590
656
  # return custom bucket value
591
657
  options[:bucket_value]
658
+ },
659
+
660
+ # cleanup when module is removed or SDK is closed
661
+ close: -> {
662
+ # cleanup here
592
663
  }
593
664
  }
594
665
  ```
595
666
 
596
- ### Registering hooks
667
+ The module API passed to `setup` exposes:
668
+
669
+ - `get_revision`
670
+ - `on_diagnostic`
671
+ - `report_diagnostic`
597
672
 
598
- You can register hooks at the time of SDK initialization:
673
+ ### Registering modules
674
+
675
+ You can register modules at the time of SDK initialization:
599
676
 
600
677
  ```ruby
601
678
  require 'featurevisor'
602
679
 
603
- f = Featurevisor.create_instance(
604
- hooks: [my_custom_hook]
680
+ f = Featurevisor.create_featurevisor(
681
+ modules: [my_custom_module]
605
682
  )
606
683
  ```
607
684
 
608
685
  Or after initialization:
609
686
 
610
687
  ```ruby
611
- f.add_hook(my_custom_hook)
688
+ remove_module = f.add_module(my_custom_module)
689
+
690
+ # remove later by calling the returned function
691
+ remove_module.call
692
+
693
+ # or remove by name
694
+ f.remove_module('my-custom-module')
612
695
  ```
613
696
 
614
697
  ## Child instance
@@ -681,15 +764,27 @@ Additional options that are available:
681
764
  ```bash
682
765
  $ bundle exec featurevisor test \
683
766
  --projectDirectoryPath="/absolute/path/to/your/featurevisor/project" \
684
- --quiet|verbose \
767
+ --quiet|--verbose \
685
768
  --onlyFailures \
686
769
  --keyPattern="myFeatureKey" \
687
770
  --assertionPattern="#1"
688
771
  ```
689
772
 
773
+ The Ruby test runner builds base datafiles and Target datafiles in memory via `npx featurevisor build --json`. When an assertion contains `target`, it is evaluated against the matching Target datafile.
774
+
775
+ All three commands accept repeatable `--target=<target>` options. `test` builds only the selected Target datafiles and runs untargeted assertions plus assertions for those targets. `benchmark` and `assess-distribution` run independently against every selected Target datafile. Without `--target`, existing project-wide behavior is preserved. Project definitions, test specs, Target discovery, and datafile generation continue to come from the Node.js CLI.
776
+
777
+ ### Test against local monorepo's example-1
778
+
779
+ ```bash
780
+ $ cd /absolute/path/to/featurevisor-ruby
781
+ $ bundle exec ruby bin/featurevisor test --projectDirectoryPath=/Users/fahad/Projects/featurevisor/featurevisor/examples/example-1 --onlyFailures
782
+ $ make test-example-1
783
+ ```
784
+
690
785
  ### Benchmark
691
786
 
692
- Learn more about benchmarking [here](https://featurevisor.com/docs/cmd/#benchmarking).
787
+ Learn more about benchmarking [here](https://featurevisor.com/docs/cli/#benchmarking).
693
788
 
694
789
  ```bash
695
790
  $ bundle exec featurevisor benchmark \
@@ -702,7 +797,7 @@ $ bundle exec featurevisor benchmark \
702
797
 
703
798
  ### Assess distribution
704
799
 
705
- Learn more about assessing distribution [here](https://featurevisor.com/docs/cmd/#assess-distribution).
800
+ Learn more about assessing distribution [here](https://featurevisor.com/docs/cli/#assess-distribution).
706
801
 
707
802
  ```bash
708
803
  $ bundle exec featurevisor assess-distribution \
@@ -716,6 +811,8 @@ $ bundle exec featurevisor assess-distribution \
716
811
  --n=1000
717
812
  ```
718
813
 
814
+ <!-- FEATUREVISOR_DOCS_END -->
815
+
719
816
  ## Development
720
817
 
721
818
  ### Setting up
data/bin/cli.rb CHANGED
@@ -7,12 +7,13 @@ module FeaturevisorCLI
7
7
  attr_accessor :command, :assertion_pattern, :context, :environment, :feature,
8
8
  :key_pattern, :n, :only_failures, :quiet, :variable, :variation,
9
9
  :verbose, :inflate, :show_datafile, :schema_version, :project_directory_path,
10
- :populate_uuid
10
+ :populate_uuid, :with_scopes, :with_tags, :targets
11
11
 
12
12
  def initialize
13
13
  @n = 1000
14
14
  @project_directory_path = Dir.pwd
15
15
  @populate_uuid = []
16
+ @targets = []
16
17
  end
17
18
  end
18
19
 
@@ -82,10 +83,18 @@ module FeaturevisorCLI
82
83
  options.show_datafile = true
83
84
  end
84
85
 
85
- opts.on("--schemaVersion=VERSION", "Schema version") do |v|
86
+ opts.on("--schemaVersion=VERSION", "--schema-version=VERSION", "Legacy schema version option accepted and ignored") do |v|
86
87
  options.schema_version = v
87
88
  end
88
89
 
90
+ opts.on("--with-scopes", "--withScopes", "Legacy scope option accepted and ignored") do
91
+ options.with_scopes = true
92
+ end
93
+
94
+ opts.on("--with-tags", "--withTags", "Legacy tag option accepted and ignored") do
95
+ options.with_tags = true
96
+ end
97
+
89
98
  opts.on("--projectDirectoryPath=PATH", "Project directory path") do |v|
90
99
  options.project_directory_path = v
91
100
  end
@@ -94,6 +103,10 @@ module FeaturevisorCLI
94
103
  options.populate_uuid << v
95
104
  end
96
105
 
106
+ opts.on("--target=TARGET", "Target datafile; repeat for multiple targets") do |v|
107
+ options.targets << v unless options.targets.include?(v)
108
+ end
109
+
97
110
  opts.on("-h", "--help", "Show this help message") do
98
111
  puts opts
99
112
  exit
@@ -29,13 +29,21 @@ module FeaturevisorCLI
29
29
  exit 1
30
30
  end
31
31
 
32
- puts ""
33
- puts "Assessing distribution for feature: \"#{@options.feature}\"..."
34
- puts ""
35
-
36
- # Parse context if provided
37
32
  context = parse_context
38
33
 
34
+ targets = resolve_targets
35
+ (targets.empty? ? [nil] : targets).each { |target| run_for_target(context, target) }
36
+ end
37
+
38
+ private
39
+
40
+ def run_for_target(context, target)
41
+ puts "\nAssess Featurevisor distribution"
42
+ puts " Feature: #{@options.feature}"
43
+ puts " Environment: #{@options.environment}"
44
+ puts " Target: #{target}" if target
45
+ puts " Iterations: #{@options.n}"
46
+
39
47
  # Print context information
40
48
  if @options.context
41
49
  puts "Against context: #{@options.context}"
@@ -47,10 +55,10 @@ module FeaturevisorCLI
47
55
  puts ""
48
56
 
49
57
  # Build datafile
50
- datafile = build_datafile(@options.environment)
58
+ datafile = build_datafile(@options.environment, target)
51
59
 
52
60
  # Create SDK instance
53
- instance = create_instance(datafile)
61
+ instance = create_featurevisor(datafile)
54
62
 
55
63
  # Check if feature has variations
56
64
  feature = instance.get_feature(@options.feature)
@@ -104,7 +112,21 @@ module FeaturevisorCLI
104
112
  end
105
113
  end
106
114
 
107
- private
115
+ def resolve_targets
116
+ return [] if @options.targets.empty?
117
+ stdout, stderr, status = Open3.capture3("npx", "featurevisor", "list", "--targets", "--json", chdir: @project_path)
118
+ unless status.success?
119
+ puts stderr
120
+ exit 1
121
+ end
122
+ available = JSON.parse(stdout).map { |item| item.is_a?(Hash) ? (item["key"] || item["name"]) : item }
123
+ unknown = @options.targets.find { |target| !available.include?(target) }
124
+ if unknown
125
+ puts "Unknown target \"#{unknown}\". Available targets: #{available.empty? ? "none" : available.join(", ")}."
126
+ exit 1
127
+ end
128
+ @options.targets
129
+ end
108
130
 
109
131
  def parse_context
110
132
  if @options.context
@@ -121,15 +143,12 @@ module FeaturevisorCLI
121
143
  end
122
144
  end
123
145
 
124
- def build_datafile(environment)
146
+ def build_datafile(environment, target = nil)
125
147
  puts "Building datafile for environment: #{environment}..."
126
148
 
127
149
  # Build the command similar to Go implementation
128
150
  command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--json"]
129
-
130
- if @options.schema_version
131
- command_parts << "--schemaVersion=#{@options.schema_version}"
132
- end
151
+ command_parts << "--target=#{target}" if target
133
152
 
134
153
  if @options.inflate
135
154
  command_parts << "--inflate=#{@options.inflate}"
@@ -159,9 +178,9 @@ module FeaturevisorCLI
159
178
  [stdout, stderr, exit_status.exitstatus]
160
179
  end
161
180
 
162
- def create_instance(datafile)
181
+ def create_featurevisor(datafile)
163
182
  # Create SDK instance
164
- Featurevisor.create_instance(
183
+ Featurevisor.create_featurevisor(
165
184
  datafile: datafile,
166
185
  log_level: get_logger_level
167
186
  )