rage-rb 1.26.1 → 1.28.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/CHANGELOG.md +30 -1
- data/CONTRIBUTING.md +10 -1
- data/README.md +1 -1
- data/exe/rage +1 -1
- data/lib/rage/cable/channel.rb +7 -11
- data/lib/rage/cli/base.rb +27 -0
- data/lib/rage/cli/code_generator.rb +58 -0
- data/lib/rage/cli/new_app_generator.rb +66 -0
- data/lib/rage/cli/openapi.rb +20 -0
- data/lib/rage/cli/skills.rb +222 -220
- data/lib/rage/cli.rb +12 -143
- data/lib/rage/code_loader.rb +5 -1
- data/lib/rage/configuration.rb +73 -2
- data/lib/rage/cookies.rb +11 -9
- data/lib/rage/daemon.rb +5 -2
- data/lib/rage/deferred/backends/disk.rb +544 -199
- data/lib/rage/deferred/backends/nil.rb +16 -2
- data/lib/rage/deferred/deferred.rb +5 -1
- data/lib/rage/deferred/queue.rb +8 -3
- data/lib/rage/deferred/task.rb +1 -0
- data/lib/rage/fiber_scheduler.rb +17 -13
- data/lib/rage/hooks.rb +6 -1
- data/lib/rage/logger/logger.rb +55 -4
- data/lib/rage/openapi/index.html.erb +1 -1
- data/lib/rage/openapi/openapi.rb +14 -5
- data/lib/rage/openapi/parsers/ext/blueprinter.rb +20 -12
- data/lib/rage/router/README.md +1 -1
- data/lib/rage/router/backend.rb +8 -4
- data/lib/rage/router/dsl.rb +5 -7
- data/lib/rage/tasks.rb +5 -0
- data/lib/rage/telemetry/capacity.rb +42 -0
- data/lib/rage/telemetry/telemetry.rb +19 -0
- data/lib/rage/version.rb +1 -1
- data/lib/rage-rb.rb +3 -0
- metadata +8 -3
data/lib/rage/cli/skills.rb
CHANGED
|
@@ -1,291 +1,293 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
desc "update", "Update installed skills"
|
|
24
|
-
option :verbose, desc: "Debug output"
|
|
25
|
-
option :json, type: :boolean, desc: "Output JSON for programmatic use"
|
|
26
|
-
def update
|
|
27
|
-
skills_destinations = Dir.glob(".*/skills/#{SKILLS_DIR}")
|
|
28
|
-
debug { "Existing skills installations found: #{skills_destinations}" }
|
|
29
|
-
|
|
30
|
-
if skills_destinations.empty?
|
|
31
|
-
log "No existing installation found. Running fresh install...\n\n"
|
|
32
|
-
return install
|
|
3
|
+
module Rage::CLI
|
|
4
|
+
class Skills < Base
|
|
5
|
+
SKILLS_DIR = "rage-framework"
|
|
6
|
+
VERSION_FILE = ".version"
|
|
7
|
+
|
|
8
|
+
desc "install", "Install skills for coding agents"
|
|
9
|
+
option :verbose, desc: "Debug output"
|
|
10
|
+
def install
|
|
11
|
+
installation_path = choose_installation_path
|
|
12
|
+
return unless installation_path
|
|
13
|
+
|
|
14
|
+
skills_version = fetch_skills_version
|
|
15
|
+
say "Downloading skills..."
|
|
16
|
+
install_skills(installation_path, skills_version)
|
|
17
|
+
|
|
18
|
+
say "#{set_color("✓", :green)} Installed Rage skills #{set_color(skills_version, :bold)} to #{set_color(installation_path, :cyan)}."
|
|
19
|
+
say "#{set_color("✓", :green)} Skills are now available to your coding agent."
|
|
20
|
+
rescue => e
|
|
21
|
+
say_error(e)
|
|
33
22
|
end
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
|
|
24
|
+
desc "update", "Update installed skills"
|
|
25
|
+
option :verbose, desc: "Debug output"
|
|
26
|
+
option :json, type: :boolean, desc: "Output JSON for programmatic use"
|
|
27
|
+
def update
|
|
28
|
+
skills_destinations = Dir.glob(".*/skills/#{SKILLS_DIR}")
|
|
29
|
+
debug { "Existing skills installations found: #{skills_destinations}" }
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if current_version == skills_version
|
|
43
|
-
log "#{set_color(destination, :cyan)}: already up to date."
|
|
44
|
-
next
|
|
31
|
+
if skills_destinations.empty?
|
|
32
|
+
log "No existing installation found. Running fresh install...\n\n"
|
|
33
|
+
return install
|
|
45
34
|
end
|
|
46
35
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
updated_paths << destination
|
|
50
|
-
end
|
|
36
|
+
skills_version = fetch_skills_version
|
|
37
|
+
updated_paths = []
|
|
51
38
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
39
|
+
skills_destinations.each do |destination|
|
|
40
|
+
version_file = File.join(destination, VERSION_FILE)
|
|
41
|
+
current_version = File.exist?(version_file) ? File.read(version_file).strip : nil
|
|
55
42
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
)
|
|
61
|
-
rescue => e
|
|
62
|
-
if options[:json]
|
|
63
|
-
json_output(status: "error", message: e.message)
|
|
64
|
-
exit 1
|
|
65
|
-
else
|
|
66
|
-
say_error(e)
|
|
67
|
-
end
|
|
68
|
-
end
|
|
43
|
+
if current_version == skills_version
|
|
44
|
+
log "#{set_color(destination, :cyan)}: already up to date."
|
|
45
|
+
next
|
|
46
|
+
end
|
|
69
47
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
48
|
+
log "Updating #{set_color(destination, :cyan)}..."
|
|
49
|
+
install_skills(destination, skills_version)
|
|
50
|
+
updated_paths << destination
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if updated_paths.any?
|
|
54
|
+
log "#{set_color("✓", :green)} Updated #{updated_paths.size} installation#{"s" if updated_paths.size > 1} to #{set_color(skills_version, :bold)}."
|
|
55
|
+
end
|
|
74
56
|
|
|
75
|
-
|
|
57
|
+
json_output(
|
|
58
|
+
status: updated_paths.any? ? "updated" : "up_to_date",
|
|
59
|
+
version: skills_version,
|
|
60
|
+
paths: skills_destinations
|
|
61
|
+
)
|
|
62
|
+
rescue => e
|
|
76
63
|
if options[:json]
|
|
77
|
-
|
|
64
|
+
json_output(status: "error", message: e.message)
|
|
65
|
+
exit 1
|
|
78
66
|
else
|
|
79
|
-
|
|
67
|
+
say_error(e)
|
|
80
68
|
end
|
|
81
69
|
end
|
|
82
70
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def say_error(error)
|
|
90
|
-
say "#{set_color("𐄂 Error:", :red, :bold)} #{error.message}"
|
|
91
|
-
debug { "#{error.class}: #{error.message}\n#{error.backtrace.join("\n")}" }
|
|
92
|
-
end
|
|
71
|
+
no_commands do
|
|
72
|
+
def debug
|
|
73
|
+
puts("* #{yield}") if options[:verbose]
|
|
74
|
+
end
|
|
93
75
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
["2", "GitHub Copilot", ".github/skills"],
|
|
98
|
-
["3", "Cursor", ".cursor/skills"],
|
|
99
|
-
["4", "Amp/Codex", ".agents/skills"],
|
|
100
|
-
["5", "Antigravity", ".agent/skills"],
|
|
101
|
-
["6", "Gemini CLI", ".gemini/skills"],
|
|
102
|
-
["7", "Windsurf", ".windsurf/skills"],
|
|
103
|
-
["8", "OpenCode", ".opencode/skills"],
|
|
104
|
-
["9", "Other", ".claude/skills"]
|
|
105
|
-
]
|
|
106
|
-
|
|
107
|
-
display_options = ([["Option", "Coding Agent", "Installation Path"]] + agent_options).map.with_index do |(option, agent, path), index|
|
|
108
|
-
if index == 0
|
|
109
|
-
[set_color(option, :bold), set_color(agent, :bold), set_color(path, :bold)]
|
|
76
|
+
def log(message)
|
|
77
|
+
if options[:json]
|
|
78
|
+
warn(message)
|
|
110
79
|
else
|
|
111
|
-
|
|
80
|
+
say(message)
|
|
112
81
|
end
|
|
113
82
|
end
|
|
114
83
|
|
|
115
|
-
|
|
116
|
-
|
|
84
|
+
def json_output(data)
|
|
85
|
+
return unless options[:json]
|
|
86
|
+
require "json"
|
|
87
|
+
puts JSON.generate(data)
|
|
88
|
+
end
|
|
117
89
|
|
|
118
|
-
|
|
90
|
+
def say_error(error)
|
|
91
|
+
say "#{set_color("𐄂 Error:", :red, :bold)} #{error.message}"
|
|
92
|
+
debug { "#{error.class}: #{error.message}\n#{error.backtrace.join("\n")}" }
|
|
93
|
+
end
|
|
119
94
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
95
|
+
def choose_installation_path
|
|
96
|
+
agent_options = [
|
|
97
|
+
["1", "Claude Code", ".claude/skills"],
|
|
98
|
+
["2", "GitHub Copilot", ".github/skills"],
|
|
99
|
+
["3", "Cursor", ".cursor/skills"],
|
|
100
|
+
["4", "Amp/Codex", ".agents/skills"],
|
|
101
|
+
["5", "Antigravity", ".agent/skills"],
|
|
102
|
+
["6", "Gemini CLI", ".gemini/skills"],
|
|
103
|
+
["7", "Windsurf", ".windsurf/skills"],
|
|
104
|
+
["8", "OpenCode", ".opencode/skills"],
|
|
105
|
+
["9", "Other", ".claude/skills"]
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
display_options = ([["Option", "Coding Agent", "Installation Path"]] + agent_options).map.with_index do |(option, agent, path), index|
|
|
109
|
+
if index == 0
|
|
110
|
+
[set_color(option, :bold), set_color(agent, :bold), set_color(path, :bold)]
|
|
111
|
+
else
|
|
112
|
+
[set_color(option, :bold), agent, set_color(path, :cyan)]
|
|
113
|
+
end
|
|
124
114
|
end
|
|
125
|
-
end
|
|
126
115
|
|
|
127
|
-
|
|
128
|
-
|
|
116
|
+
print_ansi_table(display_options)
|
|
117
|
+
agent_choice = ask(set_color("Select your coding agent (1-#{agent_options[-1][0]}):", :bold), default: "1")
|
|
129
118
|
|
|
130
|
-
|
|
131
|
-
unless answer.downcase.start_with?("y")
|
|
132
|
-
say "Installation cancelled."
|
|
133
|
-
return nil
|
|
134
|
-
end
|
|
119
|
+
installation_path = ".claude/skills"
|
|
135
120
|
|
|
136
|
-
|
|
137
|
-
|
|
121
|
+
agent_options.each do |option, agent, path|
|
|
122
|
+
if agent_choice == option || agent.downcase.include?(agent_choice.downcase)
|
|
123
|
+
installation_path = path
|
|
124
|
+
break
|
|
125
|
+
end
|
|
126
|
+
end
|
|
138
127
|
|
|
139
|
-
|
|
140
|
-
|
|
128
|
+
say "\n#{set_color("Source:", :bold)} #{set_color("https://github.com/rage-rb/skills", :cyan)}"
|
|
129
|
+
say "#{set_color("Destination:", :bold)} #{set_color(installation_path, :cyan)}"
|
|
141
130
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
131
|
+
answer = ask(set_color("? ", :green, :bold) + set_color("Proceed with installation? [Y/n]", :bold), default: "y")
|
|
132
|
+
unless answer.downcase.start_with?("y")
|
|
133
|
+
say "Installation cancelled."
|
|
134
|
+
return nil
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
File.join(installation_path, SKILLS_DIR)
|
|
147
138
|
end
|
|
148
139
|
|
|
149
|
-
|
|
140
|
+
def fetch_skills_version
|
|
141
|
+
require "json"
|
|
150
142
|
|
|
151
|
-
|
|
143
|
+
manifest = begin
|
|
144
|
+
JSON.parse(fetch("https://rage-rb.github.io/skills/manifest.json"))
|
|
145
|
+
rescue => e
|
|
146
|
+
debug { "#{e.class} (#{e.message}):\n#{e.backtrace.join("\n")}" }
|
|
147
|
+
raise "Could not download skills manifest. Please check your network connection."
|
|
148
|
+
end
|
|
152
149
|
|
|
153
|
-
|
|
154
|
-
matched_major, matched_minor = manifest["versions"].
|
|
155
|
-
keys.
|
|
156
|
-
map { |v| v.split(".").map(&:to_i) }.
|
|
157
|
-
select { |_major, _| _major == major }.
|
|
158
|
-
select { |_, _minor| _minor <= minor }.
|
|
159
|
-
max_by { |_, _minor| _minor }
|
|
150
|
+
major, minor, _ = Rage::VERSION.split(".").map(&:to_i)
|
|
160
151
|
|
|
161
|
-
|
|
162
|
-
manifest["versions"]["#{matched_major}.#{matched_minor}"]
|
|
163
|
-
else
|
|
164
|
-
raise "No skills available for Rage #{major}.x."
|
|
165
|
-
end
|
|
166
|
-
end
|
|
152
|
+
debug { "Rage::VERSION: #{Rage::VERSION}; Manifest: #{manifest["versions"]}" }
|
|
167
153
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
154
|
+
# Find the closest matching version: same major, highest minor <= current
|
|
155
|
+
matched_major, matched_minor = manifest["versions"].
|
|
156
|
+
keys.
|
|
157
|
+
map { |v| v.split(".").map(&:to_i) }.
|
|
158
|
+
select { |_major, _| _major == major }.
|
|
159
|
+
select { |_, _minor| _minor <= minor }.
|
|
160
|
+
max_by { |_, _minor| _minor }
|
|
174
161
|
|
|
175
|
-
|
|
162
|
+
if matched_major && matched_minor
|
|
163
|
+
manifest["versions"]["#{matched_major}.#{matched_minor}"]
|
|
164
|
+
else
|
|
165
|
+
raise "No skills available for Rage #{major}.x."
|
|
166
|
+
end
|
|
167
|
+
end
|
|
176
168
|
|
|
177
|
-
|
|
178
|
-
|
|
169
|
+
def install_skills(installation_path, version)
|
|
170
|
+
require "zlib"
|
|
171
|
+
require "rubygems/package"
|
|
172
|
+
require "fileutils"
|
|
173
|
+
require "stringio"
|
|
174
|
+
require "digest"
|
|
179
175
|
|
|
180
|
-
|
|
176
|
+
Thread.report_on_exception = false
|
|
181
177
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
end
|
|
178
|
+
artifact_request = Thread.new { fetch("https://github.com/rage-rb/skills/releases/download/#{version}/skills.tar.gz") }
|
|
179
|
+
checksum_request = Thread.new { fetch("https://github.com/rage-rb/skills/releases/download/#{version}/checksums.txt") }
|
|
185
180
|
|
|
186
|
-
|
|
187
|
-
if Digest::SHA256.hexdigest(artifact) != sha
|
|
188
|
-
raise "Download verification failed. Please try again."
|
|
189
|
-
end
|
|
181
|
+
artifact, checksum = artifact_request.value, checksum_request.value
|
|
190
182
|
|
|
191
|
-
|
|
192
|
-
|
|
183
|
+
if artifact.nil? || checksum.nil?
|
|
184
|
+
raise "Could not download the skills package. Please check your network connection and try again."
|
|
185
|
+
end
|
|
193
186
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
end
|
|
187
|
+
sha, _ = checksum.split
|
|
188
|
+
if Digest::SHA256.hexdigest(artifact) != sha
|
|
189
|
+
raise "Download verification failed. Please try again."
|
|
190
|
+
end
|
|
199
191
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
tar.each do |entry|
|
|
203
|
-
path = File.join(destination, entry.full_name)
|
|
192
|
+
destination = File.expand_path(installation_path)
|
|
193
|
+
FileUtils.mkdir_p(destination)
|
|
204
194
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
195
|
+
# Clear existing contents but keep the directory intact
|
|
196
|
+
Dir.children(destination).each do |child|
|
|
197
|
+
debug { "Removing #{child}" }
|
|
198
|
+
FileUtils.rm_rf(File.join(destination, child))
|
|
199
|
+
end
|
|
208
200
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
201
|
+
Zlib::GzipReader.wrap(StringIO.new(artifact)) do |gz|
|
|
202
|
+
Gem::Package::TarReader.new(gz) do |tar|
|
|
203
|
+
tar.each do |entry|
|
|
204
|
+
path = File.join(destination, entry.full_name)
|
|
205
|
+
|
|
206
|
+
unless File.expand_path(path).start_with?("#{destination}/")
|
|
207
|
+
raise "Invalid archive: contains files outside the destination directory."
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
if entry.directory?
|
|
211
|
+
debug { "Created directory #{entry.full_name}" }
|
|
212
|
+
FileUtils.mkdir_p(path)
|
|
213
|
+
elsif entry.file?
|
|
214
|
+
debug { "Written file #{entry.full_name}" }
|
|
215
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
216
|
+
File.binwrite(path, entry.read)
|
|
217
|
+
end
|
|
216
218
|
end
|
|
217
219
|
end
|
|
218
220
|
end
|
|
219
|
-
end
|
|
220
221
|
|
|
221
|
-
|
|
222
|
-
|
|
222
|
+
File.write(File.join(destination, VERSION_FILE), version)
|
|
223
|
+
end
|
|
223
224
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
225
|
+
def fetch(uri, retries = 2)
|
|
226
|
+
response = request(uri)
|
|
227
|
+
return response if response
|
|
227
228
|
|
|
228
|
-
|
|
229
|
-
|
|
229
|
+
retries > 0 ? fetch(uri, retries - 1) : nil
|
|
230
|
+
end
|
|
230
231
|
|
|
231
|
-
|
|
232
|
-
|
|
232
|
+
def request(uri, limit = 3)
|
|
233
|
+
require "net/http"
|
|
233
234
|
|
|
234
|
-
|
|
235
|
+
raise "Too many HTTP redirects" if limit == 0
|
|
235
236
|
|
|
236
|
-
|
|
237
|
+
debug { "Fetching #{uri[0..100]}" }
|
|
237
238
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
parsed_uri = URI(uri)
|
|
240
|
+
http = Net::HTTP.new(parsed_uri.host, parsed_uri.port)
|
|
241
|
+
http.open_timeout = 5
|
|
242
|
+
http.read_timeout = 5
|
|
242
243
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
244
|
+
if parsed_uri.scheme == "https"
|
|
245
|
+
http.use_ssl = true
|
|
246
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
|
247
|
+
http.cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths)
|
|
248
|
+
end
|
|
248
249
|
|
|
249
|
-
|
|
250
|
+
response = http.request(Net::HTTP::Get.new(parsed_uri))
|
|
250
251
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
252
|
+
case response
|
|
253
|
+
when Net::HTTPSuccess
|
|
254
|
+
response.body.force_encoding("ASCII-8BIT")
|
|
255
|
+
when Net::HTTPRedirection
|
|
256
|
+
debug { "Redirected to #{response["Location"][0..100]}" }
|
|
257
|
+
request(response["Location"], limit - 1)
|
|
258
|
+
end
|
|
257
259
|
end
|
|
258
|
-
end
|
|
259
260
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
261
|
+
# print a table stripping ANSI codes to ensure paddings are based on visible length
|
|
262
|
+
def print_ansi_table(rows)
|
|
263
|
+
return if rows.empty?
|
|
263
264
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
265
|
+
widths = rows.each_with_object([]) do |row, maxima|
|
|
266
|
+
row.each_with_index do |column, index|
|
|
267
|
+
visible_width = strip_ansi(column.to_s).size
|
|
268
|
+
maxima[index] = [maxima[index] || 0, visible_width].max
|
|
269
|
+
end
|
|
268
270
|
end
|
|
269
|
-
end
|
|
270
271
|
|
|
271
|
-
|
|
272
|
-
|
|
272
|
+
border = "+" + widths.map { |width| "-" * (width + 2) }.join("+") + "+"
|
|
273
|
+
say(border)
|
|
274
|
+
|
|
275
|
+
rows.each do |row|
|
|
276
|
+
formatted_cells = widths.each_index.map do |index|
|
|
277
|
+
cell = row[index].to_s
|
|
278
|
+
cell_padding = widths[index] - strip_ansi(cell).size
|
|
279
|
+
" #{cell}#{" " * cell_padding} "
|
|
280
|
+
end
|
|
273
281
|
|
|
274
|
-
|
|
275
|
-
formatted_cells = widths.each_index.map do |index|
|
|
276
|
-
cell = row[index].to_s
|
|
277
|
-
cell_padding = widths[index] - strip_ansi(cell).size
|
|
278
|
-
" #{cell}#{" " * cell_padding} "
|
|
282
|
+
say("|#{formatted_cells.join("|")}|")
|
|
279
283
|
end
|
|
280
284
|
|
|
281
|
-
say(
|
|
285
|
+
say(border)
|
|
282
286
|
end
|
|
283
287
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
def strip_ansi(text)
|
|
288
|
-
text.gsub(/\e\[[0-9;]*m/, "")
|
|
288
|
+
def strip_ansi(text)
|
|
289
|
+
text.gsub(/\e\[[0-9;]*m/, "")
|
|
290
|
+
end
|
|
289
291
|
end
|
|
290
292
|
end
|
|
291
293
|
end
|