featurevisor 3.0.0 → 4.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 +4 -4
- data/README.md +307 -193
- data/bin/cli.rb +84 -17
- data/bin/commands/assess_distribution.rb +10 -52
- data/bin/commands/benchmark.rb +10 -48
- data/bin/commands/node_cli.rb +20 -0
- data/bin/commands/runtime_project.rb +63 -0
- data/bin/commands/test.rb +771 -259
- data/bin/commands.rb +1 -0
- data/bin/segment_tests.rb +89 -0
- data/bin/test_checks.rb +767 -0
- data/lib/featurevisor/child_instance.rb +15 -2
- data/lib/featurevisor/compare_versions.rb +18 -36
- data/lib/featurevisor/conditions.rb +91 -9
- data/lib/featurevisor/dependencies.rb +150 -0
- data/lib/featurevisor/emitter.rb +4 -0
- data/lib/featurevisor/evaluate.rb +64 -115
- data/lib/featurevisor/evaluation_data_provider.rb +100 -51
- data/lib/featurevisor/evaluation_session.rb +191 -0
- data/lib/featurevisor/events.rb +39 -100
- data/lib/featurevisor/instance.rb +258 -59
- data/lib/featurevisor/modules.rb +6 -46
- data/lib/featurevisor/mutations.rb +213 -0
- data/lib/featurevisor/normalize.rb +105 -0
- data/lib/featurevisor/unified_conditions.rb +79 -0
- data/lib/featurevisor/version.rb +1 -1
- data/lib/featurevisor.rb +7 -0
- metadata +10 -1
data/bin/cli.rb
CHANGED
|
@@ -7,7 +7,9 @@ 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, :with_scopes, :with_tags, :targets
|
|
10
|
+
:populate_uuid, :with_scopes, :with_tags, :targets, :entity_type, :set,
|
|
11
|
+
:json, :pretty, :case_id, :coverage, :uncovered,
|
|
12
|
+
:project_directory_path_explicit
|
|
11
13
|
|
|
12
14
|
def initialize
|
|
13
15
|
@n = 1000
|
|
@@ -26,12 +28,15 @@ module FeaturevisorCLI
|
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
options.command = args[0]
|
|
31
|
+
unless %w[test benchmark assess-distribution].include?(options.command)
|
|
32
|
+
raise ArgumentError, "Unknown command: #{options.command}"
|
|
33
|
+
end
|
|
29
34
|
remaining_args = args[1..-1]
|
|
30
35
|
|
|
31
36
|
OptionParser.new do |opts|
|
|
32
|
-
opts.banner = "Usage: featurevisor
|
|
37
|
+
opts.banner = "Usage: featurevisor #{options.command} [options]"
|
|
33
38
|
|
|
34
|
-
opts.on("--assertionPattern=PATTERN", "Assertion pattern") do |v|
|
|
39
|
+
opts.on("--assertion-pattern=PATTERN", "--assertionPattern=PATTERN", "Assertion pattern") do |v|
|
|
35
40
|
options.assertion_pattern = v
|
|
36
41
|
end
|
|
37
42
|
|
|
@@ -39,23 +44,31 @@ module FeaturevisorCLI
|
|
|
39
44
|
options.context = v
|
|
40
45
|
end
|
|
41
46
|
|
|
42
|
-
opts.on("--environment=ENV", "
|
|
47
|
+
opts.on("--environment=ENV", "Configured environment (omit for flat projects)") do |v|
|
|
43
48
|
options.environment = v
|
|
44
49
|
end
|
|
45
50
|
|
|
46
51
|
opts.on("--feature=FEATURE", "Feature key") do |v|
|
|
47
|
-
options.feature = v
|
|
52
|
+
options.feature = nonempty_value("--feature", v)
|
|
48
53
|
end
|
|
49
54
|
|
|
50
|
-
opts.on("--keyPattern=PATTERN", "Key pattern") do |v|
|
|
55
|
+
opts.on("--key-pattern=PATTERN", "--keyPattern=PATTERN", "Key pattern") do |v|
|
|
51
56
|
options.key_pattern = v
|
|
52
57
|
end
|
|
53
58
|
|
|
54
|
-
opts.on("-
|
|
55
|
-
options.
|
|
59
|
+
opts.on("--entity-type=TYPE", "--entityType=TYPE", %w[feature segment variable scenario], "Filter tests by entity type") do |v|
|
|
60
|
+
options.entity_type = v
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
opts.on("--iterations=N", "--n=N", "Number of iterations (default: 1000)") do |v|
|
|
64
|
+
options.n = positive_iterations(v)
|
|
56
65
|
end
|
|
57
66
|
|
|
58
|
-
opts.on("
|
|
67
|
+
opts.on("-n N", "Number of iterations (default: 1000)") do |v|
|
|
68
|
+
options.n = positive_iterations(v.delete_prefix("="))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
opts.on("--only-failures", "--onlyFailures", "Only show failures") do
|
|
59
72
|
options.only_failures = true
|
|
60
73
|
end
|
|
61
74
|
|
|
@@ -64,7 +77,7 @@ module FeaturevisorCLI
|
|
|
64
77
|
end
|
|
65
78
|
|
|
66
79
|
opts.on("--variable=VARIABLE", "Variable key") do |v|
|
|
67
|
-
options.variable = v
|
|
80
|
+
options.variable = nonempty_value("--variable", v)
|
|
68
81
|
end
|
|
69
82
|
|
|
70
83
|
opts.on("--variation", "Variation mode") do
|
|
@@ -76,10 +89,11 @@ module FeaturevisorCLI
|
|
|
76
89
|
end
|
|
77
90
|
|
|
78
91
|
opts.on("--inflate=N", Integer, "Inflate mode") do |v|
|
|
92
|
+
raise OptionParser::InvalidArgument, "--inflate must be a positive integer" unless v.positive?
|
|
79
93
|
options.inflate = v
|
|
80
94
|
end
|
|
81
95
|
|
|
82
|
-
opts.on("--showDatafile", "Show datafile content for each test") do
|
|
96
|
+
opts.on("--show-datafile", "--showDatafile", "Show datafile content for each test") do
|
|
83
97
|
options.show_datafile = true
|
|
84
98
|
end
|
|
85
99
|
|
|
@@ -95,29 +109,79 @@ module FeaturevisorCLI
|
|
|
95
109
|
options.with_tags = true
|
|
96
110
|
end
|
|
97
111
|
|
|
98
|
-
opts.on("--projectDirectoryPath=PATH", "Project directory path") do |v|
|
|
99
|
-
options.project_directory_path = v
|
|
112
|
+
opts.on("--project-directory-path=PATH", "--projectDirectoryPath=PATH", "--root-directory-path=PATH", "--rootDirectoryPath=PATH", "Project directory path") do |v|
|
|
113
|
+
options.project_directory_path = nonempty_value("--project-directory-path", v)
|
|
114
|
+
options.project_directory_path_explicit = true
|
|
100
115
|
end
|
|
101
116
|
|
|
102
|
-
opts.on("--populateUuid=KEY", "Populate UUID for attribute key") do |v|
|
|
103
|
-
options.populate_uuid << v
|
|
117
|
+
opts.on("--populate-uuid=KEY", "--populateUuid=KEY", "Populate UUID for attribute key") do |v|
|
|
118
|
+
options.populate_uuid << nonempty_value("--populate-uuid", v)
|
|
104
119
|
end
|
|
105
120
|
|
|
106
121
|
opts.on("--target=TARGET", "Target datafile; repeat for multiple targets") do |v|
|
|
122
|
+
v = nonempty_value("--target", v)
|
|
107
123
|
options.targets << v unless options.targets.include?(v)
|
|
108
124
|
end
|
|
109
125
|
|
|
126
|
+
opts.on("--set=SET", "Project set") do |v|
|
|
127
|
+
options.set = v
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
opts.on("--json", "Print a versioned JSON test report") do
|
|
131
|
+
options.json = true
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
opts.on("--pretty", "Pretty print JSON output") do
|
|
135
|
+
options.pretty = true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
opts.on("--case=ID", "Run one exact expanded test case") do |v|
|
|
139
|
+
options.case_id = v
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
opts.on("--coverage", "Report observed rule and override selections") do
|
|
143
|
+
options.coverage = true
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
opts.on("--uncovered", "List features and variables without associated tests") do
|
|
147
|
+
options.uncovered = true
|
|
148
|
+
end
|
|
149
|
+
|
|
110
150
|
opts.on("-h", "--help", "Show this help message") do
|
|
111
151
|
puts opts
|
|
112
152
|
exit
|
|
113
153
|
end
|
|
114
154
|
end.parse!(remaining_args)
|
|
115
155
|
|
|
156
|
+
unless remaining_args.empty?
|
|
157
|
+
raise OptionParser::InvalidArgument, "Unexpected arguments: #{remaining_args.join(' ')}"
|
|
158
|
+
end
|
|
159
|
+
unless options.context.nil?
|
|
160
|
+
context = JSON.parse(options.context)
|
|
161
|
+
raise ArgumentError, "--context must be a JSON object" unless context.is_a?(Hash)
|
|
162
|
+
end
|
|
116
163
|
options
|
|
117
164
|
end
|
|
165
|
+
|
|
166
|
+
def self.positive_iterations(value)
|
|
167
|
+
parsed = Integer(value, 10)
|
|
168
|
+
raise ArgumentError unless parsed.positive?
|
|
169
|
+
parsed
|
|
170
|
+
rescue ArgumentError
|
|
171
|
+
raise OptionParser::InvalidArgument, "--n must be a positive integer"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def self.nonempty_value(name, value)
|
|
175
|
+
if value.empty? || value.start_with?("--")
|
|
176
|
+
raise OptionParser::InvalidArgument, "#{name} requires a nonempty value"
|
|
177
|
+
end
|
|
178
|
+
value
|
|
179
|
+
end
|
|
118
180
|
end
|
|
119
181
|
|
|
120
182
|
def self.run(args)
|
|
183
|
+
return show_help if args.empty? || args == ["--help"] || args == ["-h"]
|
|
184
|
+
|
|
121
185
|
options = Parser.parse(args)
|
|
122
186
|
|
|
123
187
|
case options.command
|
|
@@ -130,6 +194,9 @@ module FeaturevisorCLI
|
|
|
130
194
|
else
|
|
131
195
|
show_help
|
|
132
196
|
end
|
|
197
|
+
rescue ArgumentError, OptionParser::ParseError, JSON::ParserError, SystemCallError => e
|
|
198
|
+
warn "Error: #{e.message}"
|
|
199
|
+
exit 1
|
|
133
200
|
end
|
|
134
201
|
|
|
135
202
|
def self.show_help
|
|
@@ -146,10 +213,10 @@ module FeaturevisorCLI
|
|
|
146
213
|
puts ""
|
|
147
214
|
puts "Examples:"
|
|
148
215
|
puts " featurevisor test"
|
|
149
|
-
puts " featurevisor test --
|
|
216
|
+
puts " featurevisor test --key-pattern=pattern"
|
|
150
217
|
puts " featurevisor benchmark --feature=myFeature --environment=dev --n=10000"
|
|
151
218
|
puts " featurevisor assess-distribution --feature=myFeature --n=10000"
|
|
152
219
|
puts ""
|
|
153
|
-
puts "Note: benchmark requires --
|
|
220
|
+
puts "Note: benchmark requires --feature or --variable; --environment is required only when configured"
|
|
154
221
|
end
|
|
155
222
|
end
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
require "securerandom"
|
|
3
3
|
require "open3"
|
|
4
|
+
require_relative "runtime_project"
|
|
4
5
|
|
|
5
6
|
module FeaturevisorCLI
|
|
6
7
|
module Commands
|
|
7
8
|
class AssessDistribution
|
|
9
|
+
include RuntimeProject
|
|
8
10
|
# UUID_LENGTHS matches the TypeScript implementation
|
|
9
11
|
UUID_LENGTHS = [4, 2, 2, 2, 6]
|
|
10
12
|
|
|
@@ -19,11 +21,6 @@ module FeaturevisorCLI
|
|
|
19
21
|
|
|
20
22
|
def run
|
|
21
23
|
# Validate required options
|
|
22
|
-
unless @options.environment
|
|
23
|
-
puts "Error: --environment is required for assess-distribution command"
|
|
24
|
-
exit 1
|
|
25
|
-
end
|
|
26
|
-
|
|
27
24
|
unless @options.feature
|
|
28
25
|
puts "Error: --feature is required for assess-distribution command"
|
|
29
26
|
exit 1
|
|
@@ -31,8 +28,10 @@ module FeaturevisorCLI
|
|
|
31
28
|
|
|
32
29
|
context = parse_context
|
|
33
30
|
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
each_project do
|
|
32
|
+
targets = resolve_targets
|
|
33
|
+
(targets.empty? ? [nil] : targets).each { |target| run_for_target(context, target) }
|
|
34
|
+
end
|
|
36
35
|
end
|
|
37
36
|
|
|
38
37
|
private
|
|
@@ -40,7 +39,7 @@ module FeaturevisorCLI
|
|
|
40
39
|
def run_for_target(context, target)
|
|
41
40
|
puts "\nAssess Featurevisor distribution"
|
|
42
41
|
puts " Feature: #{@options.feature}"
|
|
43
|
-
puts " Environment: #{@options.environment}"
|
|
42
|
+
puts " Environment: #{@options.environment}" if @options.environment
|
|
44
43
|
puts " Target: #{target}" if target
|
|
45
44
|
puts " Iterations: #{@options.n}"
|
|
46
45
|
|
|
@@ -112,26 +111,12 @@ module FeaturevisorCLI
|
|
|
112
111
|
end
|
|
113
112
|
end
|
|
114
113
|
|
|
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
|
|
130
114
|
|
|
131
115
|
def parse_context
|
|
132
116
|
if @options.context
|
|
133
117
|
begin
|
|
134
118
|
context = JSON.parse(@options.context)
|
|
119
|
+
raise ArgumentError, "--context must be a JSON object" unless context.is_a?(Hash)
|
|
135
120
|
# Convert string keys to symbols for the SDK
|
|
136
121
|
context.transform_keys(&:to_sym)
|
|
137
122
|
rescue JSON::ParserError => e
|
|
@@ -143,38 +128,11 @@ module FeaturevisorCLI
|
|
|
143
128
|
end
|
|
144
129
|
end
|
|
145
130
|
|
|
146
|
-
def build_datafile(environment, target = nil)
|
|
147
|
-
puts "Building datafile for environment: #{environment}..."
|
|
148
|
-
|
|
149
|
-
# Build the command similar to Go implementation
|
|
150
|
-
command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--json"]
|
|
151
|
-
command_parts << "--target=#{target}" if target
|
|
152
|
-
|
|
153
|
-
if @options.inflate
|
|
154
|
-
command_parts << "--inflate=#{@options.inflate}"
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
command = command_parts.join(" ")
|
|
158
|
-
|
|
159
|
-
stdout, stderr, exit_status = execute_command(command)
|
|
160
|
-
|
|
161
|
-
if exit_status != 0
|
|
162
|
-
puts "Error: Command failed with exit code #{exit_status}"
|
|
163
|
-
puts "Command: #{command}"
|
|
164
|
-
puts "Stderr: #{stderr}"
|
|
165
|
-
exit 1
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
begin
|
|
169
|
-
JSON.parse(stdout, symbolize_names: true)
|
|
170
|
-
rescue JSON::ParserError => e
|
|
171
|
-
puts "Error: Failed to parse datafile JSON: #{e.message}"
|
|
172
|
-
exit 1
|
|
173
|
-
end
|
|
174
|
-
end
|
|
175
131
|
|
|
176
132
|
def execute_command(command)
|
|
177
133
|
stdout, stderr, exit_status = Open3.capture3(command)
|
|
134
|
+
stdout.force_encoding(Encoding::UTF_8)
|
|
135
|
+
stderr.force_encoding(Encoding::UTF_8)
|
|
178
136
|
[stdout, stderr, exit_status.exitstatus]
|
|
179
137
|
end
|
|
180
138
|
|
data/bin/commands/benchmark.rb
CHANGED
|
@@ -2,10 +2,12 @@ require "benchmark"
|
|
|
2
2
|
require "json"
|
|
3
3
|
require "time"
|
|
4
4
|
require "open3"
|
|
5
|
+
require_relative "runtime_project"
|
|
5
6
|
|
|
6
7
|
module FeaturevisorCLI
|
|
7
8
|
module Commands
|
|
8
9
|
class Benchmark
|
|
10
|
+
include RuntimeProject
|
|
9
11
|
def self.run(options)
|
|
10
12
|
new(options).run
|
|
11
13
|
end
|
|
@@ -17,19 +19,16 @@ module FeaturevisorCLI
|
|
|
17
19
|
|
|
18
20
|
def run
|
|
19
21
|
# Validate required options
|
|
20
|
-
unless @options.environment
|
|
21
|
-
puts "Error: --environment is required for benchmark command"
|
|
22
|
-
exit 1
|
|
23
|
-
end
|
|
24
|
-
|
|
25
22
|
unless @options.feature || @options.variable
|
|
26
23
|
puts "Error: --feature or --variable is required for benchmark command"
|
|
27
24
|
exit 1
|
|
28
25
|
end
|
|
29
26
|
|
|
30
27
|
context = parse_context
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
each_project do
|
|
29
|
+
targets = resolve_targets
|
|
30
|
+
(targets.empty? ? [nil] : targets).each { |target| run_for_target(context, target) }
|
|
31
|
+
end
|
|
33
32
|
end
|
|
34
33
|
|
|
35
34
|
private
|
|
@@ -43,7 +42,7 @@ module FeaturevisorCLI
|
|
|
43
42
|
puts "\nBenchmark Featurevisor feature"
|
|
44
43
|
puts " Feature: #{@options.feature}" if @options.feature
|
|
45
44
|
puts " Global variable: #{@options.variable}" if @options.variable && !@options.feature
|
|
46
|
-
puts " Environment: #{@options.environment}"
|
|
45
|
+
puts " Environment: #{@options.environment}" if @options.environment
|
|
47
46
|
puts " Target: #{target}" if target
|
|
48
47
|
puts " Iterations: #{@options.n}"
|
|
49
48
|
puts " Build duration: #{datafile_build_duration_ms}ms"
|
|
@@ -85,26 +84,12 @@ module FeaturevisorCLI
|
|
|
85
84
|
puts "Maximum duration: #{format_duration_ms(output[:max_duration])}"
|
|
86
85
|
end
|
|
87
86
|
|
|
88
|
-
def resolve_targets
|
|
89
|
-
return [] if @options.targets.empty?
|
|
90
|
-
stdout, stderr, status = Open3.capture3("npx", "featurevisor", "list", "--targets", "--json", chdir: @project_path)
|
|
91
|
-
unless status.success?
|
|
92
|
-
puts stderr
|
|
93
|
-
exit 1
|
|
94
|
-
end
|
|
95
|
-
available = JSON.parse(stdout).map { |target| target.is_a?(Hash) ? (target["key"] || target["name"]) : target }
|
|
96
|
-
unknown = @options.targets.find { |target| !available.include?(target) }
|
|
97
|
-
if unknown
|
|
98
|
-
puts "Unknown target \"#{unknown}\". Available targets: #{available.empty? ? "none" : available.join(", ")}."
|
|
99
|
-
exit 1
|
|
100
|
-
end
|
|
101
|
-
@options.targets
|
|
102
|
-
end
|
|
103
87
|
|
|
104
88
|
def parse_context
|
|
105
89
|
if @options.context
|
|
106
90
|
begin
|
|
107
91
|
context = JSON.parse(@options.context)
|
|
92
|
+
raise ArgumentError, "--context must be a JSON object" unless context.is_a?(Hash)
|
|
108
93
|
# Convert string keys to symbols for the SDK
|
|
109
94
|
context.transform_keys(&:to_sym)
|
|
110
95
|
rescue JSON::ParserError => e
|
|
@@ -116,35 +101,12 @@ module FeaturevisorCLI
|
|
|
116
101
|
end
|
|
117
102
|
end
|
|
118
103
|
|
|
119
|
-
def build_datafile(environment, target = nil)
|
|
120
|
-
puts "Building datafile for environment: #{environment}..."
|
|
121
|
-
|
|
122
|
-
command_parts = ["cd", @project_path, "&&", "npx", "featurevisor", "build", "--environment=#{environment}", "--json"]
|
|
123
|
-
command_parts << "--target=#{target}" if target
|
|
124
|
-
|
|
125
|
-
# Add inflate if specified
|
|
126
|
-
if @options.inflate && @options.inflate > 0
|
|
127
|
-
command_parts << "--inflate=#{@options.inflate}"
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
command = command_parts.join(" ")
|
|
131
|
-
|
|
132
|
-
# Execute the command and capture output
|
|
133
|
-
datafile_output = execute_command(command)
|
|
134
|
-
|
|
135
|
-
# Parse the JSON output
|
|
136
|
-
begin
|
|
137
|
-
JSON.parse(datafile_output, symbolize_names: true)
|
|
138
|
-
rescue JSON::ParserError => e
|
|
139
|
-
puts "Error: Failed to parse datafile JSON: #{e.message}"
|
|
140
|
-
puts "Command output: #{datafile_output}"
|
|
141
|
-
exit 1
|
|
142
|
-
end
|
|
143
|
-
end
|
|
144
104
|
|
|
145
105
|
def execute_command(command)
|
|
146
106
|
# Execute the command and capture stdout/stderr
|
|
147
107
|
stdout, stderr, status = Open3.capture3(command)
|
|
108
|
+
stdout.force_encoding(Encoding::UTF_8)
|
|
109
|
+
stderr.force_encoding(Encoding::UTF_8)
|
|
148
110
|
|
|
149
111
|
unless status.success?
|
|
150
112
|
puts "Error: Command failed with exit code #{status.exitstatus}"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module FeaturevisorCLI
|
|
2
|
+
# Runs the project's own Node based Featurevisor CLI.
|
|
3
|
+
#
|
|
4
|
+
# Node starts the local package entry directly, with an argument list and a
|
|
5
|
+
# working directory, so no shell quoting is involved on any platform. Without a
|
|
6
|
+
# local package, npx resolves the CLI as before, through npx.cmd on Windows.
|
|
7
|
+
module NodeCli
|
|
8
|
+
def self.command(project_path)
|
|
9
|
+
directory = File.expand_path(project_path)
|
|
10
|
+
loop do
|
|
11
|
+
entry = File.join(directory, "node_modules", "@featurevisor", "cli", "bin.js")
|
|
12
|
+
return ["node", entry] if File.file?(entry)
|
|
13
|
+
parent = File.dirname(directory)
|
|
14
|
+
break if parent == directory
|
|
15
|
+
directory = parent
|
|
16
|
+
end
|
|
17
|
+
[Gem.win_platform? ? "npx.cmd" : "npx", "featurevisor"]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "open3"
|
|
3
|
+
|
|
4
|
+
module FeaturevisorCLI
|
|
5
|
+
module Commands
|
|
6
|
+
module RuntimeProject
|
|
7
|
+
def each_project
|
|
8
|
+
raise ArgumentError, "--n must be a positive integer" unless @options.n.positive?
|
|
9
|
+
if @options.variation && @options.variable
|
|
10
|
+
raise ArgumentError, "--variation and --variable cannot be combined"
|
|
11
|
+
end
|
|
12
|
+
config = project_json("config", "--json", include_set: false)
|
|
13
|
+
environments = config["environments"]
|
|
14
|
+
if environments.is_a?(Array)
|
|
15
|
+
raise ArgumentError, "Pass --environment=<environment> when selecting one datafile." unless @options.environment
|
|
16
|
+
unless environments.include?(@options.environment)
|
|
17
|
+
raise ArgumentError, "Unknown environment #{@options.environment.inspect}. Available environments: #{environments.join(', ')}."
|
|
18
|
+
end
|
|
19
|
+
elsif @options.environment
|
|
20
|
+
raise ArgumentError, "This project does not configure environments. Omit --environment."
|
|
21
|
+
end
|
|
22
|
+
sets = [@options.set]
|
|
23
|
+
if config["sets"] && !@options.set
|
|
24
|
+
directory = File.expand_path(config["setsDirectoryPath"] || "sets", @project_path)
|
|
25
|
+
sets = Dir.children(directory).select { |entry| File.directory?(File.join(directory, entry)) }.sort
|
|
26
|
+
raise ArgumentError, "No sets found in #{directory}." if sets.empty?
|
|
27
|
+
end
|
|
28
|
+
sets.each do |set|
|
|
29
|
+
@active_set = set
|
|
30
|
+
puts "Set #{set.inspect}" if set
|
|
31
|
+
yield
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def project_json(*args, include_set: true)
|
|
36
|
+
args << "--set=#{@active_set || @options.set}" if include_set && (@active_set || @options.set)
|
|
37
|
+
stdout, stderr, status = Open3.capture3(*FeaturevisorCLI::NodeCli.command(@project_path), *args, chdir: @project_path)
|
|
38
|
+
stdout.force_encoding(Encoding::UTF_8)
|
|
39
|
+
stderr.force_encoding(Encoding::UTF_8)
|
|
40
|
+
raise ArgumentError, stderr.empty? ? "Node CLI failed with exit code #{status.exitstatus}" : stderr unless status.success?
|
|
41
|
+
warn stderr unless stderr.empty?
|
|
42
|
+
JSON.parse(stdout)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def resolve_targets
|
|
46
|
+
return [] if @options.targets.empty?
|
|
47
|
+
available = project_json("list", "--targets", "--json").map { |target| target.is_a?(Hash) ? (target["key"] || target["name"]) : target }
|
|
48
|
+
unknown = @options.targets.find { |target| !available.include?(target) }
|
|
49
|
+
raise ArgumentError, "Unknown target #{unknown.inspect}. Available targets: #{available.join(', ')}." if unknown
|
|
50
|
+
@options.targets
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def build_datafile(environment, target = nil)
|
|
54
|
+
args = ["build", "--json"]
|
|
55
|
+
args << "--environment=#{environment}" if environment
|
|
56
|
+
args << "--target=#{target}" if target
|
|
57
|
+
args << "--inflate=#{@options.inflate}" if @options.inflate && @options.inflate.positive?
|
|
58
|
+
# Symbol keys are the native SDK datafile representation.
|
|
59
|
+
JSON.parse(JSON.generate(project_json(*args)), symbolize_names: true)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|