flipper 1.2.2 → 1.3.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -1
  3. data/.github/workflows/examples.yml +1 -1
  4. data/README.md +2 -0
  5. data/docs/images/banner.jpg +0 -0
  6. data/lib/flipper/adapters/actor_limit.rb +28 -0
  7. data/lib/flipper/adapters/cache_base.rb +143 -0
  8. data/lib/flipper/adapters/operation_logger.rb +18 -88
  9. data/lib/flipper/adapters/read_only.rb +6 -39
  10. data/lib/flipper/adapters/strict.rb +5 -10
  11. data/lib/flipper/adapters/wrapper.rb +54 -0
  12. data/lib/flipper/cli.rb +36 -17
  13. data/lib/flipper/cloud/configuration.rb +2 -3
  14. data/lib/flipper/cloud/telemetry/instrumenter.rb +4 -8
  15. data/lib/flipper/cloud/telemetry.rb +10 -2
  16. data/lib/flipper/engine.rb +5 -5
  17. data/lib/flipper/poller.rb +6 -5
  18. data/lib/flipper/serializers/gzip.rb +3 -5
  19. data/lib/flipper/serializers/json.rb +3 -5
  20. data/lib/flipper/spec/shared_adapter_specs.rb +17 -16
  21. data/lib/flipper/test/shared_adapter_test.rb +17 -17
  22. data/lib/flipper/typecast.rb +3 -3
  23. data/lib/flipper/version.rb +1 -1
  24. data/lib/flipper.rb +3 -1
  25. data/package-lock.json +41 -0
  26. data/package.json +10 -0
  27. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  28. data/spec/flipper/adapters/http_spec.rb +11 -2
  29. data/spec/flipper/cli_spec.rb +21 -46
  30. data/spec/flipper/cloud/configuration_spec.rb +2 -1
  31. data/spec/flipper/cloud/telemetry_spec.rb +52 -0
  32. data/spec/flipper/cloud_spec.rb +4 -2
  33. data/spec/flipper/engine_spec.rb +34 -4
  34. data/spec/flipper/middleware/memoizer_spec.rb +7 -4
  35. data/spec/support/fail_on_output.rb +8 -0
  36. data/spec/support/spec_helpers.rb +2 -1
  37. data/test/adapters/actor_limit_test.rb +20 -0
  38. data/test_rails/system/test_help_test.rb +1 -1
  39. metadata +14 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ff734c4221d1ea2694fdf4e9e463085f60242e698b8702389c53ccc6a17e87b
4
- data.tar.gz: 5666852d9f7416d30919a188c5e3c46cb202d66d332c482aba56c3d496437282
3
+ metadata.gz: 934e0e50f2aea9b4294ed435a780f5eb55ad8b547df9c80f609022b880a4dd9c
4
+ data.tar.gz: 8799605783af26860795d336a66d511f35b0f644424aca80cd3f81baaadc6475
5
5
  SHA512:
6
- metadata.gz: c48de5bfe83ff53d969d496f96a3df9335af8f1fe8d41559158930c9ae3dd94b91f245f7b4cfbceea1c85ab453a4dd94057e4a46e3f928d9b27ddc417f04c718
7
- data.tar.gz: 2ed4dd3a74bbf07fe289d57b9545e073527971e5ab7f393ee96ea8f31904a9a550db4cda0ba492f080a14b1eb8b4982eb680e88b1c2bcfeea2e74e032207b1f4
6
+ metadata.gz: d0e041360e5d15966dd4f4a39be10616aff35dd56a7491297adce42b8d13d3ba7126feaf2d3798194c72fac73afad7e2355379ff51e40481f4a0ee25c49e6c8a
7
+ data.tar.gz: c16307b9a21775b1db67e9fd99c72c5206e7b0d6e0324e7625f349228757aea1243be676ba838ac0b853a7a48469fe8536da41085982556c0492ad985daf51ca
@@ -26,6 +26,7 @@ jobs:
26
26
  --health-timeout 5s
27
27
  --health-retries 5
28
28
  strategy:
29
+ fail-fast: false
29
30
  matrix:
30
31
  ruby: ['2.6', '2.7', '3.0', '3.1', '3.2', '3.3']
31
32
  rails: ['5.2', '6.0.0', '6.1.0', '7.0.0', '7.1.0']
@@ -75,7 +76,7 @@ jobs:
75
76
  - name: Check out repository code
76
77
  uses: actions/checkout@v4
77
78
  - name: Do some action caching
78
- uses: actions/cache@v3
79
+ uses: actions/cache@v4
79
80
  with:
80
81
  path: vendor/bundle
81
82
  key: ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.rails }}-${{ hashFiles('**/Gemfile.lock') }}
@@ -58,7 +58,7 @@ jobs:
58
58
  - name: Check out repository code
59
59
  uses: actions/checkout@v4
60
60
  - name: Do some action caching
61
- uses: actions/cache@v3
61
+ uses: actions/cache@v4
62
62
  with:
63
63
  path: vendor/bundle
64
64
  key: ${{ runner.os }}-gems-${{ matrix.ruby }}-${{ matrix.rails }}-${{ hashFiles('**/Gemfile.lock') }}
data/README.md CHANGED
@@ -111,3 +111,5 @@ We also have a [free plan](https://www.flippercloud.io?utm_source=oss&utm_medium
111
111
  | ![@alexwheeler](https://avatars3.githubusercontent.com/u/3260042?s=64) | [@alexwheeler](https://github.com/alexwheeler) | api |
112
112
  | ![@thetimbanks](https://avatars1.githubusercontent.com/u/471801?s=64) | [@thetimbanks](https://github.com/thetimbanks) | ui |
113
113
  | ![@lazebny](https://avatars1.githubusercontent.com/u/6276766?s=64) | [@lazebny](https://github.com/lazebny) | docker |
114
+ | ![@pagertree](https://avatars.githubusercontent.com/u/24941240?s=64) | [@pagertree](https://github.com/pagertree) | sponsor |
115
+ | ![@kdaigle](https://avatars.githubusercontent.com/u/2501?s=64) | [@kdaigle](https://github.com/kdaigle) | sponsor |
Binary file
@@ -0,0 +1,28 @@
1
+ module Flipper
2
+ module Adapters
3
+ class ActorLimit < Wrapper
4
+ LimitExceeded = Class.new(Flipper::Error)
5
+
6
+ attr_reader :limit
7
+
8
+ def initialize(adapter, limit = 100)
9
+ super(adapter)
10
+ @limit = limit
11
+ end
12
+
13
+ def enable(feature, gate, resource)
14
+ if gate.is_a?(Flipper::Gates::Actor) && over_limit?(feature)
15
+ raise LimitExceeded, "Actor limit of #{@limit} exceeded for feature #{feature.key}. See https://www.flippercloud.io/docs/features/actors#limitations"
16
+ else
17
+ super
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def over_limit?(feature)
24
+ feature.actors_value.size >= @limit
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,143 @@
1
+ module Flipper
2
+ module Adapters
3
+ # Base class for caching adapters. Inherit from this and then override
4
+ # cache_fetch, cache_read_multi, cache_write, and cache_delete.
5
+ class CacheBase
6
+ include ::Flipper::Adapter
7
+
8
+ # Public: The adapter being cached.
9
+ attr_reader :adapter
10
+
11
+ # Public: The ActiveSupport::Cache::Store to cache with.
12
+ attr_reader :cache
13
+
14
+ # Public: The ttl for all cached data.
15
+ attr_reader :ttl
16
+
17
+ # Public: The cache key where the set of known features is cached.
18
+ attr_reader :features_cache_key
19
+
20
+ # Public: Alias expires_in to ttl for compatibility.
21
+ alias_method :expires_in, :ttl
22
+
23
+ def initialize(adapter, cache, ttl = 300, prefix: nil)
24
+ @adapter = adapter
25
+ @cache = cache
26
+ @ttl = ttl
27
+
28
+ @cache_version = 'v1'.freeze
29
+ @namespace = "flipper/#{@cache_version}"
30
+ @namespace = @namespace.prepend(prefix) if prefix
31
+ @features_cache_key = "#{@namespace}/features"
32
+ end
33
+
34
+ # Public: Expire the cache for the set of known feature names.
35
+ def expire_features_cache
36
+ cache_delete @features_cache_key
37
+ end
38
+
39
+ # Public: Expire the cache for a given feature.
40
+ def expire_feature_cache(key)
41
+ cache_delete feature_cache_key(key)
42
+ end
43
+
44
+ # Public
45
+ def features
46
+ read_feature_keys
47
+ end
48
+
49
+ # Public
50
+ def add(feature)
51
+ result = @adapter.add(feature)
52
+ expire_features_cache
53
+ result
54
+ end
55
+
56
+ # Public
57
+ def remove(feature)
58
+ result = @adapter.remove(feature)
59
+ expire_features_cache
60
+ expire_feature_cache(feature.key)
61
+ result
62
+ end
63
+
64
+ # Public
65
+ def clear(feature)
66
+ result = @adapter.clear(feature)
67
+ expire_feature_cache(feature.key)
68
+ result
69
+ end
70
+
71
+ # Public
72
+ def get(feature)
73
+ read_feature(feature)
74
+ end
75
+
76
+ # Public
77
+ def get_multi(features)
78
+ read_many_features(features)
79
+ end
80
+
81
+ # Public
82
+ def get_all
83
+ features = read_feature_keys.map { |key| Flipper::Feature.new(key, self) }
84
+ read_many_features(features)
85
+ end
86
+
87
+ # Public
88
+ def enable(feature, gate, thing)
89
+ result = @adapter.enable(feature, gate, thing)
90
+ expire_feature_cache(feature.key)
91
+ result
92
+ end
93
+
94
+ # Public
95
+ def disable(feature, gate, thing)
96
+ result = @adapter.disable(feature, gate, thing)
97
+ expire_feature_cache(feature.key)
98
+ result
99
+ end
100
+
101
+ # Public: Generate the cache key for a given feature.
102
+ #
103
+ # key - The String or Symbol feature key.
104
+ def feature_cache_key(key)
105
+ "#{@namespace}/feature/#{key}"
106
+ end
107
+
108
+ private
109
+
110
+ # Private: Returns the Set of known feature keys.
111
+ def read_feature_keys
112
+ cache_fetch(@features_cache_key) { @adapter.features }
113
+ end
114
+
115
+ # Private: Read through caching for a single feature.
116
+ def read_feature(feature)
117
+ cache_fetch(feature_cache_key(feature.key)) { @adapter.get(feature) }
118
+ end
119
+
120
+ # Private: Given an array of features, attempts to read through cache in
121
+ # as few network calls as possible.
122
+ def read_many_features(features)
123
+ keys = features.map { |feature| feature_cache_key(feature.key) }
124
+ cache_result = cache_read_multi(keys)
125
+ uncached_features = features.reject { |feature| cache_result[feature_cache_key(feature)] }
126
+
127
+ if uncached_features.any?
128
+ response = @adapter.get_multi(uncached_features)
129
+ response.each do |key, value|
130
+ cache_write feature_cache_key(key), value
131
+ cache_result[feature_cache_key(key)] = value
132
+ end
133
+ end
134
+
135
+ result = {}
136
+ features.each do |feature|
137
+ result[feature.key] = cache_result[feature_cache_key(feature.key)]
138
+ end
139
+ result
140
+ end
141
+ end
142
+ end
143
+ end
@@ -5,111 +5,34 @@ module Flipper
5
5
  # Public: Adapter that wraps another adapter and stores the operations.
6
6
  #
7
7
  # Useful in tests to verify calls and such. Never use outside of testing.
8
- class OperationLogger
9
- include Flipper::Adapter
8
+ class OperationLogger < Wrapper
10
9
 
11
10
  class Operation
12
- attr_reader :type, :args
11
+ attr_reader :type, :args, :kwargs
13
12
 
14
- def initialize(type, args)
13
+ def initialize(type, args, kwargs = {})
15
14
  @type = type
16
15
  @args = args
16
+ @kwargs = kwargs
17
17
  end
18
18
  end
19
19
 
20
- OperationTypes = [
21
- :import,
22
- :export,
23
- :features,
24
- :add,
25
- :remove,
26
- :clear,
27
- :get,
28
- :get_multi,
29
- :get_all,
30
- :enable,
31
- :disable,
32
- ].freeze
33
-
34
20
  # Internal: An array of the operations that have happened.
35
21
  attr_reader :operations
36
22
 
37
23
  # Public
38
24
  def initialize(adapter, operations = nil)
39
- @adapter = adapter
25
+ super(adapter)
40
26
  @operations = operations || []
41
27
  end
42
28
 
43
- # Public: The set of known features.
44
- def features
45
- @operations << Operation.new(:features, [])
46
- @adapter.features
47
- end
48
-
49
- # Public: Adds a feature to the set of known features.
50
- def add(feature)
51
- @operations << Operation.new(:add, [feature])
52
- @adapter.add(feature)
53
- end
54
-
55
- # Public: Removes a feature from the set of known features and clears
56
- # all the values for the feature.
57
- def remove(feature)
58
- @operations << Operation.new(:remove, [feature])
59
- @adapter.remove(feature)
60
- end
61
-
62
- # Public: Clears all the gate values for a feature.
63
- def clear(feature)
64
- @operations << Operation.new(:clear, [feature])
65
- @adapter.clear(feature)
66
- end
67
-
68
- # Public
69
- def get(feature)
70
- @operations << Operation.new(:get, [feature])
71
- @adapter.get(feature)
72
- end
73
-
74
- # Public
75
- def get_multi(features)
76
- @operations << Operation.new(:get_multi, [features])
77
- @adapter.get_multi(features)
78
- end
79
-
80
- # Public
81
- def get_all
82
- @operations << Operation.new(:get_all, [])
83
- @adapter.get_all
84
- end
85
-
86
- # Public
87
- def enable(feature, gate, thing)
88
- @operations << Operation.new(:enable, [feature, gate, thing])
89
- @adapter.enable(feature, gate, thing)
90
- end
91
-
92
- # Public
93
- def disable(feature, gate, thing)
94
- @operations << Operation.new(:disable, [feature, gate, thing])
95
- @adapter.disable(feature, gate, thing)
96
- end
97
-
98
- # Public
99
- def import(source)
100
- @operations << Operation.new(:import, [source])
101
- @adapter.import(source)
102
- end
103
-
104
- # Public
105
- def export(format: :json, version: 1)
106
- @operations << Operation.new(:export, [format, version])
107
- @adapter.export(format: format, version: version)
108
- end
109
-
110
29
  # Public: Count the number of times a certain operation happened.
111
- def count(type)
112
- type(type).size
30
+ def count(type = nil)
31
+ if type
32
+ type(type).size
33
+ else
34
+ @operations.size
35
+ end
113
36
  end
114
37
 
115
38
  # Public: Get all operations of a certain type.
@@ -131,6 +54,13 @@ module Flipper
131
54
  inspect_id = ::Kernel::format "%x", (object_id * 2)
132
55
  %(#<#{self.class}:0x#{inspect_id} @name=#{name.inspect}, @operations=#{@operations.inspect}, @adapter=#{@adapter.inspect}>)
133
56
  end
57
+
58
+ private
59
+
60
+ def wrap(method, *args, **kwargs, &block)
61
+ @operations << Operation.new(method, args, kwargs)
62
+ block.call
63
+ end
134
64
  end
135
65
  end
136
66
  end
@@ -3,8 +3,8 @@ require 'flipper'
3
3
  module Flipper
4
4
  module Adapters
5
5
  # Public: Adapter that wraps another adapter and raises for any writes.
6
- class ReadOnly
7
- include ::Flipper::Adapter
6
+ class ReadOnly < Wrapper
7
+ WRITE_METHODS = %i[add remove clear enable disable]
8
8
 
9
9
  class WriteAttempted < Error
10
10
  def initialize(message = nil)
@@ -12,49 +12,16 @@ module Flipper
12
12
  end
13
13
  end
14
14
 
15
- # Public
16
- def initialize(adapter)
17
- @adapter = adapter
18
- end
19
-
20
- def features
21
- @adapter.features
22
- end
23
-
24
15
  def read_only?
25
16
  true
26
17
  end
27
18
 
28
- def get(feature)
29
- @adapter.get(feature)
30
- end
31
-
32
- def get_multi(features)
33
- @adapter.get_multi(features)
34
- end
35
-
36
- def get_all
37
- @adapter.get_all
38
- end
39
-
40
- def add(_feature)
41
- raise WriteAttempted
42
- end
43
-
44
- def remove(_feature)
45
- raise WriteAttempted
46
- end
47
-
48
- def clear(_feature)
49
- raise WriteAttempted
50
- end
19
+ private
51
20
 
52
- def enable(_feature, _gate, _thing)
53
- raise WriteAttempted
54
- end
21
+ def wrap(method, *args, **kwargs)
22
+ raise WriteAttempted if WRITE_METHODS.include?(method)
55
23
 
56
- def disable(_feature, _gate, _thing)
57
- raise WriteAttempted
24
+ yield
58
25
  end
59
26
  end
60
27
  end
@@ -1,10 +1,8 @@
1
1
  module Flipper
2
2
  module Adapters
3
3
  # An adapter that ensures a feature exists before checking it.
4
- class Strict
5
- extend Forwardable
6
- include ::Flipper::Adapter
7
- attr_reader :name, :adapter, :handler
4
+ class Strict < Wrapper
5
+ attr_reader :handler
8
6
 
9
7
  class NotFound < ::Flipper::Error
10
8
  def initialize(name)
@@ -12,22 +10,19 @@ module Flipper
12
10
  end
13
11
  end
14
12
 
15
- def_delegators :@adapter, :features, :get_all, :add, :remove, :clear, :enable, :disable
16
-
17
13
  def initialize(adapter, handler = nil, &block)
18
- @name = :strict
19
- @adapter = adapter
14
+ super(adapter)
20
15
  @handler = block || handler
21
16
  end
22
17
 
23
18
  def get(feature)
24
19
  assert_feature_exists(feature)
25
- @adapter.get(feature)
20
+ super
26
21
  end
27
22
 
28
23
  def get_multi(features)
29
24
  features.each { |feature| assert_feature_exists(feature) }
30
- @adapter.get_multi(features)
25
+ super
31
26
  end
32
27
 
33
28
  private
@@ -0,0 +1,54 @@
1
+ module Flipper
2
+ module Adapters
3
+ # A base class for any adapter that wraps another adapter. By default, all methods
4
+ # delegate to the wrapped adapter. Implement `#wrap` to customize the behavior of
5
+ # all delegated methods, or override individual methods as needed.
6
+ class Wrapper
7
+ include Flipper::Adapter
8
+
9
+ METHODS = [
10
+ :import,
11
+ :export,
12
+ :features,
13
+ :add,
14
+ :remove,
15
+ :clear,
16
+ :get,
17
+ :get_multi,
18
+ :get_all,
19
+ :enable,
20
+ :disable,
21
+ ].freeze
22
+
23
+ attr_reader :adapter
24
+
25
+ def initialize(adapter)
26
+ @adapter = adapter
27
+ end
28
+
29
+ METHODS.each do |method|
30
+ if RUBY_VERSION >= '3.0'
31
+ define_method(method) do |*args, **kwargs|
32
+ wrap(method, *args, **kwargs) { @adapter.public_send(method, *args, **kwargs) }
33
+ end
34
+ else
35
+ define_method(method) do |*args|
36
+ wrap(method, *args) { @adapter.public_send(method, *args) }
37
+ end
38
+ end
39
+ end
40
+
41
+ # Override this method to customize the behavior of all delegated methods, and just yield to
42
+ # the block to call the wrapped adapter.
43
+ if RUBY_VERSION >= '3.0'
44
+ def wrap(method, *args, **kwargs, &block)
45
+ block.call
46
+ end
47
+ else
48
+ def wrap(method, *args, &block)
49
+ block.call
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
data/lib/flipper/cli.rb CHANGED
@@ -9,7 +9,9 @@ module Flipper
9
9
  # Path to the local Rails application's environment configuration.
10
10
  DEFAULT_REQUIRE = "./config/environment"
11
11
 
12
- def initialize
12
+ attr_accessor :shell
13
+
14
+ def initialize(stdout: $stdout, stderr: $stderr, shell: Bundler::Thor::Base.shell.new)
13
15
  super
14
16
 
15
17
  # Program is always flipper, no matter how it's invoked
@@ -17,6 +19,10 @@ module Flipper
17
19
  @require = ENV.fetch("FLIPPER_REQUIRE", DEFAULT_REQUIRE)
18
20
  @commands = {}
19
21
 
22
+ # Extend whatever shell to support output redirection
23
+ @shell = shell.extend(ShellOutput)
24
+ shell.redirect(stdout: stdout, stderr: stderr)
25
+
20
26
  %w[enable disable].each do |action|
21
27
  command action do |c|
22
28
  c.banner = "Usage: #{c.program_name} [options] <feature>"
@@ -40,10 +46,12 @@ module Flipper
40
46
  begin
41
47
  values << Flipper::Expression.build(JSON.parse(expression))
42
48
  rescue JSON::ParserError => e
43
- warn "JSON parse error: #{e.message}"
49
+ ui.error "JSON parse error #{e.message}"
50
+ ui.trace(e)
44
51
  exit 1
45
52
  rescue ArgumentError => e
46
- warn "Invalid expression: #{e.message}"
53
+ ui.error "Invalid expression: #{e.message}"
54
+ ui.trace(e)
47
55
  exit 1
48
56
  end
49
57
  end
@@ -57,7 +65,7 @@ module Flipper
57
65
  values.each { |value| f.send(action, value) }
58
66
  end
59
67
 
60
- puts feature_details(f)
68
+ ui.info feature_details(f)
61
69
  end
62
70
  end
63
71
  end
@@ -65,21 +73,21 @@ module Flipper
65
73
  command 'list' do |c|
66
74
  c.description = "List defined features"
67
75
  c.action do
68
- puts feature_summary(Flipper.features)
76
+ ui.info feature_summary(Flipper.features)
69
77
  end
70
78
  end
71
79
 
72
80
  command 'show' do |c|
73
81
  c.description = "Show a defined feature"
74
82
  c.action do |feature|
75
- puts feature_details(Flipper.feature(feature))
83
+ ui.info feature_details(Flipper.feature(feature))
76
84
  end
77
85
  end
78
86
 
79
87
  command 'help' do |c|
80
88
  c.load_environment = false
81
89
  c.action do |command = nil|
82
- puts command ? @commands[command].help : help
90
+ ui.info command ? @commands[command].help : help
83
91
  end
84
92
  end
85
93
 
@@ -89,7 +97,7 @@ module Flipper
89
97
 
90
98
  # Options available on all commands
91
99
  on_tail('-h', '--help', 'Print help message') do
92
- puts help
100
+ ui.info help
93
101
  exit
94
102
  end
95
103
 
@@ -114,15 +122,15 @@ module Flipper
114
122
  load_environment! if @commands[command].load_environment
115
123
  @commands[command].run(args)
116
124
  else
117
- puts help
125
+ ui.info help
118
126
 
119
127
  if command
120
- warn "Unknown command: #{command}"
128
+ ui.error "Unknown command: #{command}"
121
129
  exit 1
122
130
  end
123
131
  end
124
132
  rescue OptionParser::InvalidOption => e
125
- warn e.message
133
+ ui.error e.message
126
134
  exit 1
127
135
  end
128
136
 
@@ -138,7 +146,7 @@ module Flipper
138
146
  # Ensure all of flipper gets loaded if it hasn't already.
139
147
  require 'flipper'
140
148
  rescue LoadError => e
141
- warn e.message
149
+ ui.error e.message
142
150
  exit 1
143
151
  end
144
152
 
@@ -170,7 +178,7 @@ module Flipper
170
178
  end
171
179
 
172
180
  colorize("%-#{padding}s" % feature.key, [:BOLD, :WHITE]) + " is #{summary}"
173
- end
181
+ end.join("\n")
174
182
  end
175
183
 
176
184
  def feature_details(feature)
@@ -210,10 +218,12 @@ module Flipper
210
218
  end
211
219
 
212
220
  def colorize(text, colors)
213
- if defined?(Bundler)
214
- Bundler.ui.add_color(text, *colors)
215
- else
216
- text
221
+ ui.add_color(text, *colors)
222
+ end
223
+
224
+ def ui
225
+ @ui ||= Bundler::UI::Shell.new.tap do |ui|
226
+ ui.shell = shell
217
227
  end
218
228
  end
219
229
 
@@ -221,6 +231,15 @@ module Flipper
221
231
  text.gsub(/^/, " " * spaces)
222
232
  end
223
233
 
234
+ # Redirect the shell's output to the given stdout and stderr streams
235
+ module ShellOutput
236
+ attr_reader :stdout, :stderr
237
+
238
+ def redirect(stdout: $stdout, stderr: $stderr)
239
+ @stdout, @stderr = stdout, stderr
240
+ end
241
+ end
242
+
224
243
  class Command < OptionParser
225
244
  attr_accessor :description, :load_environment
226
245
 
@@ -174,7 +174,7 @@ module Flipper
174
174
  end
175
175
 
176
176
  def setup_log(options)
177
- set_option :logging_enabled, options, default: true, typecast: :boolean
177
+ set_option :logging_enabled, options, default: false, typecast: :boolean
178
178
  set_option :logger, options, from_env: false, default: -> {
179
179
  if logging_enabled
180
180
  Logger.new(STDOUT)
@@ -214,8 +214,7 @@ module Flipper
214
214
  Telemetry.instance_for(self)
215
215
  }
216
216
 
217
- # This is alpha. Don't use this unless you are me. And you are not me.
218
- set_option :telemetry_enabled, options, default: false, typecast: :boolean
217
+ set_option :telemetry_enabled, options, default: true, typecast: :boolean
219
218
  instrumenter = options.fetch(:instrumenter, Instrumenters::Noop)
220
219
  @instrumenter = if telemetry_enabled
221
220
  Telemetry::Instrumenter.new(self, instrumenter)