console 1.29.2 → 1.36.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
- checksums.yaml.gz.sig +0 -0
- data/context/command-line.md +47 -0
- data/context/configuration.md +23 -0
- data/context/events.md +71 -0
- data/context/getting-started.md +170 -0
- data/context/index.yaml +28 -0
- data/context/integration.md +37 -0
- data/lib/console/capture.rb +29 -3
- data/lib/console/clock.rb +9 -3
- data/lib/console/compatible/logger.rb +34 -3
- data/lib/console/config.rb +96 -0
- data/lib/console/event/failure.rb +33 -4
- data/lib/console/event/generic.rb +22 -1
- data/lib/console/event/spawn.rb +39 -7
- data/lib/console/event.rb +7 -1
- data/lib/console/filter.rb +83 -7
- data/lib/console/format/safe.rb +205 -58
- data/lib/console/format.rb +5 -1
- data/lib/console/interface.rb +18 -10
- data/lib/console/logger.rb +25 -38
- data/lib/console/output/default.rb +31 -6
- data/lib/console/output/failure.rb +9 -2
- data/lib/console/output/null.rb +5 -2
- data/lib/console/output/sensitive.rb +42 -1
- data/lib/console/output/serialized.rb +32 -7
- data/lib/console/output/split.rb +12 -1
- data/lib/console/output/terminal.rb +68 -15
- data/lib/console/output/wrapper.rb +12 -1
- data/lib/console/output.rb +14 -3
- data/lib/console/progress.rb +56 -9
- data/lib/console/resolver.rb +19 -2
- data/lib/console/terminal/formatter/failure.rb +18 -6
- data/lib/console/terminal/formatter/progress.rb +16 -3
- data/lib/console/terminal/formatter/spawn.rb +16 -5
- data/lib/console/terminal/formatter.rb +16 -0
- data/lib/console/terminal/text.rb +66 -20
- data/lib/console/terminal/xterm.rb +18 -3
- data/lib/console/terminal.rb +7 -9
- data/lib/console/version.rb +2 -2
- data/lib/console/warn.rb +2 -2
- data/lib/console.rb +2 -1
- data/license.md +6 -3
- data/readme.md +60 -6
- data/releases.md +115 -1
- data.tar.gz.sig +0 -0
- metadata +20 -12
- metadata.gz.sig +0 -0
data/lib/console/format/safe.rb
CHANGED
|
@@ -1,29 +1,179 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2023-
|
|
4
|
+
# Copyright, 2023-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require "json"
|
|
7
7
|
|
|
8
8
|
module Console
|
|
9
|
+
# @namespace
|
|
9
10
|
module Format
|
|
10
|
-
#
|
|
11
|
-
#
|
|
11
|
+
# A safe format for converting objects to strings.
|
|
12
|
+
#
|
|
13
|
+
# Handles issues like circular references, encoding errors, excessive nesting depth, and excessive output size.
|
|
12
14
|
class Safe
|
|
13
|
-
|
|
15
|
+
# The JSON fragment used as the truncation marker when dropped fields cannot be named.
|
|
16
|
+
TRUNCATED = "\"truncated\":true"
|
|
17
|
+
|
|
18
|
+
# Create a new safe format.
|
|
19
|
+
#
|
|
20
|
+
# @parameter format [JSON] The format to use for serialization.
|
|
21
|
+
# @parameter depth_limit [Integer] The maximum depth to recurse into objects (the JSON `max_nesting`).
|
|
22
|
+
# @parameter size_limit [Integer | Nil] The maximum byte size of the serialized output, or `nil` to disable size limiting. Limits below {TRUNCATED} (the minimal marker) cannot be honoured.
|
|
23
|
+
# @parameter encoding [Encoding] The encoding to use for strings.
|
|
24
|
+
# @parameter limit [Integer | Nil] Deprecated alias for `depth_limit`.
|
|
25
|
+
def initialize(format: ::JSON, depth_limit: 12, size_limit: 16 * 1024, encoding: ::Encoding::UTF_8, limit: nil)
|
|
26
|
+
if limit
|
|
27
|
+
warn "Console::Format::Safe `limit:` is deprecated, use `depth_limit:` instead.", uplevel: 1, category: :deprecated
|
|
28
|
+
depth_limit = limit
|
|
29
|
+
end
|
|
30
|
+
|
|
14
31
|
@format = format
|
|
15
|
-
@
|
|
32
|
+
@depth_limit = depth_limit
|
|
33
|
+
@size_limit = size_limit
|
|
16
34
|
@encoding = encoding
|
|
17
35
|
end
|
|
18
36
|
|
|
37
|
+
# @attribute [Integer] The maximum depth to recurse into objects.
|
|
38
|
+
attr :depth_limit
|
|
39
|
+
|
|
40
|
+
# @attribute [Integer | Nil] The maximum byte size of the serialized output.
|
|
41
|
+
attr :size_limit
|
|
42
|
+
|
|
43
|
+
# Dump the given object to a string.
|
|
44
|
+
#
|
|
45
|
+
# The common case is a single fast serialization. If that fails (e.g. circular
|
|
46
|
+
# references, excessive nesting, or encoding errors) or its output exceeds
|
|
47
|
+
# {size_limit}, it falls back to {safe_dump}, which rebuilds the record
|
|
48
|
+
# field-by-field within the limit.
|
|
49
|
+
#
|
|
50
|
+
# @parameter object [Object] The object to dump.
|
|
51
|
+
# @returns [String] The dumped object.
|
|
19
52
|
def dump(object)
|
|
20
|
-
@format.dump(object, @
|
|
21
|
-
|
|
22
|
-
@
|
|
53
|
+
buffer = @format.dump(object, @depth_limit)
|
|
54
|
+
|
|
55
|
+
if @size_limit and buffer.bytesize > @size_limit
|
|
56
|
+
return safe_dump(object)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return buffer
|
|
60
|
+
rescue SystemStackError, StandardError
|
|
61
|
+
return safe_dump(object)
|
|
23
62
|
end
|
|
24
63
|
|
|
25
64
|
private
|
|
26
65
|
|
|
66
|
+
# Produce a safe, size-limited serialization of the given object. This is the
|
|
67
|
+
# fallback path, used both when direct serialization fails (an exception) and
|
|
68
|
+
# when its output exceeds {size_limit}.
|
|
69
|
+
#
|
|
70
|
+
# Each top-level value is serialized independently and defensively, so a single
|
|
71
|
+
# un-serializable or oversized value cannot break or bloat the whole record.
|
|
72
|
+
# Whenever a field is degraded, the reason is recorded in a trailing `"truncated"`
|
|
73
|
+
# object that maps the field name to why it was truncated:
|
|
74
|
+
#
|
|
75
|
+
# - `"key": true` — the value was dropped because it did not fit the size limit.
|
|
76
|
+
# - `"key": {error}` — the value could not be serialized directly; a safe
|
|
77
|
+
# representation was kept in its place and the triggering error is recorded.
|
|
78
|
+
#
|
|
79
|
+
# Fields are kept while they fit, always reserving room for at least a minimal
|
|
80
|
+
# `"truncated":true` marker. The detailed reason map is then emitted only if it
|
|
81
|
+
# fits in the remaining space; otherwise it degrades to `"truncated":true`. This
|
|
82
|
+
# is best-effort — in the worst case the per-field detail is lost — but it keeps
|
|
83
|
+
# the bookkeeping simple and the size guarantee hard.
|
|
84
|
+
#
|
|
85
|
+
# @parameter object [Object] The object to serialize.
|
|
86
|
+
# @returns [String] The safe, size-limited serialized record.
|
|
87
|
+
def safe_dump(object)
|
|
88
|
+
# Serialize hash-like objects field-by-field; anything else falls through to the
|
|
89
|
+
# error handler below, which emits a minimal truncated marker.
|
|
90
|
+
object = object.to_hash
|
|
91
|
+
|
|
92
|
+
# Serialize each field once, capturing the error for any value that could not be
|
|
93
|
+
# serialized directly. Our own "truncated" key is skipped so it is never duplicated.
|
|
94
|
+
errors = {}
|
|
95
|
+
fragments = []
|
|
96
|
+
object.each do |key, value|
|
|
97
|
+
name = key.to_s
|
|
98
|
+
next if name == "truncated"
|
|
99
|
+
|
|
100
|
+
fragment, error = dump_pair(key, value)
|
|
101
|
+
errors[name] = error_info(error) if error
|
|
102
|
+
fragments << [name, fragment]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Assemble the body, keeping each field while it fits — always reserving room for
|
|
106
|
+
# at least a minimal `"truncated":true` marker. Each truncated field's reason is
|
|
107
|
+
# collected: its error (value recovered) or `true` (dropped for size).
|
|
108
|
+
buffer = +"{"
|
|
109
|
+
first = true
|
|
110
|
+
reasons = {}
|
|
111
|
+
|
|
112
|
+
fragments.each do |name, fragment|
|
|
113
|
+
if buffer.bytesize + (first ? 0 : 1) + fragment.bytesize + TRUNCATED.bytesize + 2 <= @size_limit
|
|
114
|
+
buffer << "," unless first
|
|
115
|
+
buffer << fragment
|
|
116
|
+
first = false
|
|
117
|
+
|
|
118
|
+
# The value was kept; if it had to be recovered, note why.
|
|
119
|
+
reasons[name] = errors[name] if errors[name]
|
|
120
|
+
else
|
|
121
|
+
# The value did not fit and was dropped entirely.
|
|
122
|
+
reasons[name] = true
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
unless reasons.empty?
|
|
127
|
+
# Include the detailed reasons if they fit, otherwise fall back to the minimal
|
|
128
|
+
# marker so the truncation is still signalled.
|
|
129
|
+
detailed = "\"truncated\":#{@format.dump(reasons)}"
|
|
130
|
+
fits = buffer.bytesize + (first ? 0 : 1) + detailed.bytesize + 1 <= @size_limit
|
|
131
|
+
|
|
132
|
+
buffer << "," unless first
|
|
133
|
+
buffer << (fits ? detailed : TRUNCATED)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
buffer << "}"
|
|
137
|
+
|
|
138
|
+
return buffer
|
|
139
|
+
rescue SystemStackError, StandardError
|
|
140
|
+
return "{#{TRUNCATED}}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Serialize a single top-level `"key":value` pair, safely handling values that
|
|
144
|
+
# cannot be serialized directly.
|
|
145
|
+
#
|
|
146
|
+
# @parameter key [Object] The field key.
|
|
147
|
+
# @parameter value [Object] The field value.
|
|
148
|
+
# @returns [Array(String, Exception | Nil)] The `"key":value` fragment and the error, if recovery was needed.
|
|
149
|
+
def dump_pair(key, value)
|
|
150
|
+
value_json, error = dump_value(value)
|
|
151
|
+
|
|
152
|
+
return ["#{dump_string(String(key))}:#{value_json}", error]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Serialize a single value, falling back to a safe representation on failure.
|
|
156
|
+
#
|
|
157
|
+
# @parameter value [Object] The value to serialize.
|
|
158
|
+
# @returns [Array(String, Exception | Nil)] The serialized value and the error, if recovery was needed.
|
|
159
|
+
def dump_value(value)
|
|
160
|
+
[@format.dump(value, @depth_limit), nil]
|
|
161
|
+
rescue SystemStackError, StandardError => error
|
|
162
|
+
[@format.dump(safe_dump_recurse(value)), error]
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Serialize a string as a JSON string, encoding it safely first.
|
|
166
|
+
#
|
|
167
|
+
# @parameter value [String] The string to serialize.
|
|
168
|
+
# @returns [String] The serialized (quoted) string.
|
|
169
|
+
def dump_string(value)
|
|
170
|
+
@format.dump(value.encode(@encoding, invalid: :replace, undef: :replace))
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Filter the backtrace to remove duplicate frames and reduce verbosity.
|
|
174
|
+
#
|
|
175
|
+
# @parameter error [Exception] The exception to filter.
|
|
176
|
+
# @returns [Array(String)] The filtered backtrace.
|
|
27
177
|
def filter_backtrace(error)
|
|
28
178
|
frames = error.backtrace
|
|
29
179
|
filtered = {}
|
|
@@ -61,74 +211,71 @@ module Console
|
|
|
61
211
|
return frames
|
|
62
212
|
end
|
|
63
213
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
214
|
+
# Build a safe, primitive representation of an error for inclusion as an `"error"` field.
|
|
215
|
+
#
|
|
216
|
+
# @parameter error [Exception] The error that occurred while dumping the object.
|
|
217
|
+
# @returns [Hash] The error details (class, message, filtered backtrace).
|
|
218
|
+
def error_info(error)
|
|
219
|
+
{
|
|
69
220
|
class: safe_dump_recurse(error.class.name),
|
|
70
221
|
message: safe_dump_recurse(error.message),
|
|
71
222
|
backtrace: safe_dump_recurse(filter_backtrace(error)),
|
|
72
223
|
}
|
|
73
|
-
|
|
74
|
-
return object
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def replacement_for(object)
|
|
78
|
-
case object
|
|
79
|
-
when Array
|
|
80
|
-
"[...]"
|
|
81
|
-
when Hash
|
|
82
|
-
"{...}"
|
|
83
|
-
else
|
|
84
|
-
"..."
|
|
85
|
-
end
|
|
86
224
|
end
|
|
87
225
|
|
|
226
|
+
# Create a new hash with identity comparison.
|
|
88
227
|
def default_objects
|
|
89
228
|
Hash.new.compare_by_identity
|
|
90
229
|
end
|
|
91
230
|
|
|
92
|
-
# This will recursively generate a safe version of the object.
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return replacement_for(object)
|
|
100
|
-
end
|
|
101
|
-
|
|
231
|
+
# This will recursively generate a safe version of the object. Nested hashes and arrays will be transformed recursively. Strings will be encoded with the given encoding. Primitive values will be returned as-is. Other values will be converted using `as_json` if available, otherwise `to_s`.
|
|
232
|
+
#
|
|
233
|
+
# @parameter object [Object] The object to dump.
|
|
234
|
+
# @parameter limit [Integer] The maximum depth to recurse into objects.
|
|
235
|
+
# @parameter objects [Hash] The objects that have already been visited.
|
|
236
|
+
# @returns [Object] The dumped object as a primitive representation.
|
|
237
|
+
def safe_dump_recurse(object, limit = @depth_limit, objects = default_objects)
|
|
102
238
|
case object
|
|
103
239
|
when Hash
|
|
104
|
-
objects[object]
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
[
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
240
|
+
if limit <= 0 || objects[object]
|
|
241
|
+
return "{...}"
|
|
242
|
+
else
|
|
243
|
+
objects[object] = true
|
|
244
|
+
|
|
245
|
+
return object.to_h do |key, value|
|
|
246
|
+
[
|
|
247
|
+
String(key).encode(@encoding, invalid: :replace, undef: :replace),
|
|
248
|
+
safe_dump_recurse(value, limit - 1, objects)
|
|
249
|
+
]
|
|
250
|
+
end
|
|
111
251
|
end
|
|
112
252
|
when Array
|
|
113
|
-
objects[object]
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
253
|
+
if limit <= 0 || objects[object]
|
|
254
|
+
return "[...]"
|
|
255
|
+
else
|
|
256
|
+
objects[object] = true
|
|
257
|
+
|
|
258
|
+
return object.map do |value|
|
|
259
|
+
safe_dump_recurse(value, limit - 1, objects)
|
|
260
|
+
end
|
|
117
261
|
end
|
|
118
262
|
when String
|
|
119
|
-
object.encode(@encoding, invalid: :replace, undef: :replace)
|
|
263
|
+
return object.encode(@encoding, invalid: :replace, undef: :replace)
|
|
120
264
|
when Numeric, TrueClass, FalseClass, NilClass
|
|
121
|
-
object
|
|
265
|
+
return object
|
|
122
266
|
else
|
|
123
|
-
objects[object]
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
267
|
+
if limit <= 0 || objects[object]
|
|
268
|
+
return "..."
|
|
269
|
+
else
|
|
270
|
+
objects[object] = true
|
|
271
|
+
|
|
272
|
+
# We could do something like this but the chance `as_json` will blow up.
|
|
273
|
+
# We'd need to be extremely careful about it.
|
|
274
|
+
# if object.respond_to?(:as_json)
|
|
275
|
+
# safe_dump_recurse(object.as_json, limit - 1, objects)
|
|
276
|
+
# else
|
|
277
|
+
return safe_dump_recurse(object.to_s, limit - 1, objects)
|
|
278
|
+
end
|
|
132
279
|
end
|
|
133
280
|
end
|
|
134
281
|
end
|
data/lib/console/format.rb
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2023-
|
|
4
|
+
# Copyright, 2023-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "format/safe"
|
|
7
7
|
|
|
8
8
|
module Console
|
|
9
|
+
# @namespace
|
|
9
10
|
module Format
|
|
11
|
+
# A safe format for converting objects to strings.
|
|
12
|
+
#
|
|
13
|
+
# @returns [Console::Format::Safe]
|
|
10
14
|
def self.default
|
|
11
15
|
Safe.new(format: ::JSON)
|
|
12
16
|
end
|
data/lib/console/interface.rb
CHANGED
|
@@ -1,53 +1,61 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright 2024, by Samuel Williams.
|
|
4
|
+
# Copyright, 2024-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
require "fiber/local"
|
|
7
|
+
require_relative "config"
|
|
7
8
|
|
|
8
9
|
module Console
|
|
9
10
|
# The public logger interface.
|
|
10
11
|
module Interface
|
|
12
|
+
extend Fiber::Local
|
|
13
|
+
|
|
14
|
+
# Create a new (thread local) logger instance.
|
|
15
|
+
def self.local
|
|
16
|
+
Config::DEFAULT.make_logger
|
|
17
|
+
end
|
|
18
|
+
|
|
11
19
|
# Get the current logger instance.
|
|
12
20
|
def logger
|
|
13
|
-
|
|
21
|
+
Interface.instance
|
|
14
22
|
end
|
|
15
23
|
|
|
16
24
|
# Set the current logger instance.
|
|
17
25
|
#
|
|
18
26
|
# The current logger instance is assigned per-fiber.
|
|
19
27
|
def logger= instance
|
|
20
|
-
|
|
28
|
+
Interface.instance= instance
|
|
21
29
|
end
|
|
22
30
|
|
|
23
31
|
# Emit a debug log message.
|
|
24
32
|
def debug(...)
|
|
25
|
-
|
|
33
|
+
Interface.instance.debug(...)
|
|
26
34
|
end
|
|
27
35
|
|
|
28
36
|
# Emit an informational log message.
|
|
29
37
|
def info(...)
|
|
30
|
-
|
|
38
|
+
Interface.instance.info(...)
|
|
31
39
|
end
|
|
32
40
|
|
|
33
41
|
# Emit a warning log message.
|
|
34
42
|
def warn(...)
|
|
35
|
-
|
|
43
|
+
Interface.instance.warn(...)
|
|
36
44
|
end
|
|
37
45
|
|
|
38
46
|
# Emit an error log message.
|
|
39
47
|
def error(...)
|
|
40
|
-
|
|
48
|
+
Interface.instance.error(...)
|
|
41
49
|
end
|
|
42
50
|
|
|
43
51
|
# Emit a fatal log message.
|
|
44
52
|
def fatal(...)
|
|
45
|
-
|
|
53
|
+
Interface.instance.fatal(...)
|
|
46
54
|
end
|
|
47
55
|
|
|
48
56
|
# Emit a log message with arbitrary arguments and options.
|
|
49
57
|
def call(...)
|
|
50
|
-
|
|
58
|
+
Interface.instance.call(...)
|
|
51
59
|
end
|
|
52
60
|
end
|
|
53
61
|
end
|
data/lib/console/logger.rb
CHANGED
|
@@ -1,68 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2019-
|
|
4
|
+
# Copyright, 2019-2025, by Samuel Williams.
|
|
5
5
|
# Copyright, 2021, by Bryan Powell.
|
|
6
6
|
# Copyright, 2021, by Robert Schulze.
|
|
7
|
+
# Copyright, 2025, by Shigeru Nakajima.
|
|
8
|
+
|
|
9
|
+
require_relative "filter"
|
|
7
10
|
|
|
8
11
|
require_relative "output"
|
|
9
12
|
require_relative "output/failure"
|
|
10
13
|
|
|
11
|
-
require_relative "filter"
|
|
12
|
-
require_relative "event"
|
|
13
|
-
require_relative "resolver"
|
|
14
14
|
require_relative "progress"
|
|
15
15
|
|
|
16
|
-
require "fiber/local"
|
|
17
|
-
|
|
18
16
|
module Console
|
|
17
|
+
# The standard logger interface with support for log levels and verbosity.
|
|
18
|
+
#
|
|
19
|
+
# The log levels are: `debug`, `info`, `warn`, `error`, and `fatal`.
|
|
19
20
|
class Logger < Filter[debug: 0, info: 1, warn: 2, error: 3, fatal: 4]
|
|
20
|
-
extend Fiber::Local
|
|
21
|
-
|
|
22
21
|
# Set the default log level based on `$DEBUG` and `$VERBOSE`.
|
|
23
22
|
# You can also specify CONSOLE_LEVEL=debug or CONSOLE_LEVEL=info in environment.
|
|
24
23
|
# https://mislav.net/2011/06/ruby-verbose-mode/ has more details about how it all fits together.
|
|
25
|
-
|
|
24
|
+
#
|
|
25
|
+
# @parameter env [Hash] The environment to read the log level from.
|
|
26
|
+
# @parameter verbose [Boolean] The verbose flag.
|
|
27
|
+
# @parameter debug [Boolean] The debug flag.
|
|
28
|
+
# @returns [Integer] The default log level.
|
|
29
|
+
def self.default_log_level(env = ENV, verbose: $VERBOSE, debug: $DEBUG)
|
|
26
30
|
if level = env["CONSOLE_LEVEL"]
|
|
27
31
|
LEVELS[level.to_sym] || level.to_i
|
|
28
|
-
elsif
|
|
32
|
+
elsif debug
|
|
29
33
|
DEBUG
|
|
30
|
-
elsif
|
|
34
|
+
elsif verbose.nil?
|
|
31
35
|
WARN
|
|
32
36
|
else
|
|
33
37
|
INFO
|
|
34
38
|
end
|
|
35
39
|
end
|
|
36
40
|
|
|
37
|
-
# Controls verbose output using `$VERBOSE`.
|
|
38
|
-
def self.verbose?(env = ENV)
|
|
39
|
-
!$VERBOSE.nil? || env["CONSOLE_VERBOSE"]
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def self.default_logger(output = $stderr, env = ENV, **options)
|
|
43
|
-
if options[:verbose].nil?
|
|
44
|
-
options[:verbose] = self.verbose?(env)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
if options[:level].nil?
|
|
48
|
-
options[:level] = self.default_log_level(env)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
output = Output.new(output, env, **options)
|
|
52
|
-
|
|
53
|
-
logger = self.new(output, **options)
|
|
54
|
-
|
|
55
|
-
Resolver.default_resolver(logger)
|
|
56
|
-
|
|
57
|
-
return logger
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def self.local
|
|
61
|
-
self.default_logger
|
|
62
|
-
end
|
|
63
|
-
|
|
64
41
|
DEFAULT_LEVEL = 1
|
|
65
42
|
|
|
43
|
+
# Create a new logger.
|
|
44
|
+
#
|
|
45
|
+
# @parameter output [Console::Output] The output destination.
|
|
46
|
+
# @parameter options [Hash] Additional options.
|
|
66
47
|
def initialize(output, **options)
|
|
67
48
|
# This is the expected default behaviour, but it may be nice to have a way to override it.
|
|
68
49
|
output = Output::Failure.new(output, **options)
|
|
@@ -70,6 +51,12 @@ module Console
|
|
|
70
51
|
super(output, **options)
|
|
71
52
|
end
|
|
72
53
|
|
|
54
|
+
# Create a progress indicator for the given subject.
|
|
55
|
+
#
|
|
56
|
+
# @parameter subject [String] The subject of the progress indicator.
|
|
57
|
+
# @parameter total [Integer] The total number of items to process.
|
|
58
|
+
# @parameter options [Hash] Additional options passed to {Progress}.
|
|
59
|
+
# @returns [Progress] The progress indicator.
|
|
73
60
|
def progress(subject, total, **options)
|
|
74
61
|
options[:severity] ||= :info
|
|
75
62
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2021-
|
|
4
|
+
# Copyright, 2021-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "terminal"
|
|
7
7
|
require_relative "serialized"
|
|
@@ -9,18 +9,43 @@ require_relative "failure"
|
|
|
9
9
|
|
|
10
10
|
module Console
|
|
11
11
|
module Output
|
|
12
|
+
# Default output format selection.
|
|
12
13
|
module Default
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
# Create a new output format based on the given stream.
|
|
15
|
+
#
|
|
16
|
+
# @parameter io [IO] The output stream.
|
|
17
|
+
# @parameter env [Hash] Environment variables (defaults to ENV for testing).
|
|
18
|
+
# @parameter options [Hash] Additional options to customize the output.
|
|
19
|
+
# @returns [Console::Output::Terminal | Console::Output::Serialized] The output instance, depending on whether the `io` is a terminal or not.
|
|
20
|
+
def self.new(stream, env: ENV, **options)
|
|
21
|
+
stream ||= $stderr
|
|
15
22
|
|
|
16
|
-
if
|
|
17
|
-
output = Terminal.new(
|
|
23
|
+
if stream.tty?
|
|
24
|
+
output = Terminal.new(stream, **options)
|
|
25
|
+
elsif self.mail?(env)
|
|
26
|
+
output = Text.new(stream, **options)
|
|
27
|
+
elsif self.github_actions?(env)
|
|
28
|
+
output = XTerm.new(stream, **options)
|
|
18
29
|
else
|
|
19
|
-
output = Serialized.new(
|
|
30
|
+
output = Serialized.new(stream, **options)
|
|
20
31
|
end
|
|
21
32
|
|
|
22
33
|
return output
|
|
23
34
|
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
# Detect if we're running in a cron job or mail context where human-readable output is preferred.
|
|
39
|
+
# Cron jobs often have MAILTO set and lack TERM, or have minimal TERM values.
|
|
40
|
+
def self.mail?(env = ENV)
|
|
41
|
+
env.key?("MAILTO") && !env["MAILTO"].empty?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Detect if we're running in GitHub Actions, where human-readable output is preferred.
|
|
45
|
+
# GitHub Actions sets the GITHUB_ACTIONS environment variable to "true".
|
|
46
|
+
def self.github_actions?(env = ENV)
|
|
47
|
+
env["GITHUB_ACTIONS"] == "true"
|
|
48
|
+
end
|
|
24
49
|
end
|
|
25
50
|
end
|
|
26
51
|
end
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright,
|
|
4
|
+
# Copyright, 2024-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
require_relative "wrapper"
|
|
7
7
|
require_relative "../event/failure"
|
|
@@ -10,11 +10,18 @@ module Console
|
|
|
10
10
|
module Output
|
|
11
11
|
# A wrapper for outputting failure messages, which can include exceptions.
|
|
12
12
|
class Failure < Wrapper
|
|
13
|
+
# Create a new failure output wrapper.
|
|
13
14
|
def initialize(output, **options)
|
|
14
15
|
super(output, **options)
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
# The exception must be either the last argument or passed as an option.
|
|
19
|
+
#
|
|
20
|
+
# @parameter subject [String] The subject of the message.
|
|
21
|
+
# @parameter arguments [Array] The arguments to output.
|
|
22
|
+
# @parameter exception [Exception] The exception to output.
|
|
23
|
+
# @parameter options [Hash] Additional options to pass to the output.
|
|
24
|
+
# @parameter block [Proc] An optional block to pass to the output.
|
|
18
25
|
def call(subject = nil, *arguments, exception: nil, **options, &block)
|
|
19
26
|
if exception.nil?
|
|
20
27
|
last = arguments.last
|
|
@@ -28,7 +35,7 @@ module Console
|
|
|
28
35
|
options[:exception] = exception
|
|
29
36
|
end
|
|
30
37
|
|
|
31
|
-
super(subject, *arguments, **options)
|
|
38
|
+
super(subject, *arguments, **options, &block)
|
|
32
39
|
end
|
|
33
40
|
end
|
|
34
41
|
end
|
data/lib/console/output/null.rb
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# Released under the MIT License.
|
|
4
|
-
# Copyright, 2023-
|
|
4
|
+
# Copyright, 2023-2025, by Samuel Williams.
|
|
5
5
|
|
|
6
6
|
module Console
|
|
7
7
|
module Output
|
|
8
|
+
# A null output that does nothing.
|
|
8
9
|
class Null
|
|
10
|
+
# Create a new null output.
|
|
9
11
|
def initialize(...)
|
|
10
12
|
end
|
|
11
13
|
|
|
14
|
+
# The last output is always self.
|
|
12
15
|
def last_output
|
|
13
16
|
self
|
|
14
17
|
end
|
|
15
18
|
|
|
19
|
+
# Do nothing.
|
|
16
20
|
def call(...)
|
|
17
|
-
# Do nothing.
|
|
18
21
|
end
|
|
19
22
|
end
|
|
20
23
|
end
|