activerabbit-cli 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 +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE.txt +21 -0
- data/README.md +170 -0
- data/exe/activerabbit +6 -0
- data/lib/activerabbit/api_client.rb +306 -0
- data/lib/activerabbit/cli.rb +323 -0
- data/lib/activerabbit/commands.rb +50 -0
- data/lib/activerabbit/config.rb +87 -0
- data/lib/activerabbit/formatters.rb +327 -0
- data/lib/activerabbit/ui.rb +112 -0
- data/lib/activerabbit/version.rb +5 -0
- data/lib/activerabbit.rb +9 -0
- metadata +90 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module ActiveRabbit
|
|
7
|
+
module Formatters
|
|
8
|
+
def self.format(payload, format)
|
|
9
|
+
case format.to_s.downcase
|
|
10
|
+
when "json" then Json.format(payload)
|
|
11
|
+
when "md", "markdown" then Markdown.format(payload)
|
|
12
|
+
else Human.format(payload)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module Json
|
|
17
|
+
def self.format(payload)
|
|
18
|
+
out = {
|
|
19
|
+
"project" => payload["project"],
|
|
20
|
+
"generated_at" => payload["generated_at"],
|
|
21
|
+
"command" => payload["command"],
|
|
22
|
+
"data" => payload["data"]
|
|
23
|
+
}
|
|
24
|
+
JSON.pretty_generate(out)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module Markdown
|
|
29
|
+
def self.format(payload)
|
|
30
|
+
cmd = payload["command"]
|
|
31
|
+
data = payload["data"] || {}
|
|
32
|
+
case cmd
|
|
33
|
+
when "apps" then apps_md(data)
|
|
34
|
+
when "status" then status_md(data)
|
|
35
|
+
when "incidents" then incidents_md(data)
|
|
36
|
+
when "incident_detail" then incident_detail_md(data)
|
|
37
|
+
when "explain" then explain_md(data)
|
|
38
|
+
when "trace" then trace_md(data)
|
|
39
|
+
when "deploy_check" then deploy_check_md(data)
|
|
40
|
+
when "doctor" then doctor_md(data)
|
|
41
|
+
else generic_md(payload)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def self.apps_md(data)
|
|
46
|
+
list = data["apps"] || []
|
|
47
|
+
rows = list.map { |a| "| #{a["slug"]} | #{a["name"]} | #{a["environment"]} | #{a["error_count_24h"]} |" }
|
|
48
|
+
<<~MD
|
|
49
|
+
## Apps
|
|
50
|
+
| Slug | Name | Env | Errors (24h) |
|
|
51
|
+
|------|------|-----|-------------:|
|
|
52
|
+
#{rows.join("\n")}
|
|
53
|
+
MD
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.status_md(data)
|
|
57
|
+
top = data["top_issue"]
|
|
58
|
+
<<~MD
|
|
59
|
+
## #{data["name"]} (#{data["app"]})
|
|
60
|
+
**Health:** #{data["health"]} · **Errors (24h):** #{data["error_count_24h"]} · **p95:** #{data["p95_latency_ms"]} ms
|
|
61
|
+
**Deploy:** #{data["deploy_status"]} · Last: #{data["last_deploy_at"]}
|
|
62
|
+
|
|
63
|
+
#{top ? "**Top issue:** #{top["title"]} (#{top["count"]} events, #{top["severity"]})" : ""}
|
|
64
|
+
MD
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.incidents_md(data)
|
|
68
|
+
list = data["incidents"] || []
|
|
69
|
+
rows = list.map { |i| "| #{i["id"]} | #{i["severity"]} | #{i["title"]} | #{i["endpoint"]} | #{i["count"]} | #{i["last_seen_at"]} |" }
|
|
70
|
+
<<~MD
|
|
71
|
+
## Incidents
|
|
72
|
+
| ID | Severity | Title | Endpoint | Count | Last seen |
|
|
73
|
+
|---|---:|---|---|---:|---|
|
|
74
|
+
#{rows.join("\n")}
|
|
75
|
+
MD
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.incident_detail_md(data)
|
|
79
|
+
<<~MD
|
|
80
|
+
## #{data["exception_class"]}: #{data["message"]}
|
|
81
|
+
**ID:** #{data["id"]} · **Severity:** #{data["severity"]} · **Status:** #{data["status"]}
|
|
82
|
+
**Endpoint:** #{data["endpoint"]} · **Count:** #{data["count"]} · **Affected users:** #{data["affected_users"]}
|
|
83
|
+
|
|
84
|
+
### Stack trace
|
|
85
|
+
```
|
|
86
|
+
#{Array(data["backtrace"]).join("\n")}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
First seen: #{data["first_seen_at"]} · Last seen: #{data["last_seen_at"]}
|
|
90
|
+
MD
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def self.explain_md(data)
|
|
94
|
+
<<~MD
|
|
95
|
+
## #{data["title"]} (#{data["incident_id"]})
|
|
96
|
+
**Severity:** #{data["severity"]} · **Confidence:** #{(data["confidence_score"] * 100).to_i}%
|
|
97
|
+
|
|
98
|
+
### Root cause
|
|
99
|
+
#{data["root_cause"]}
|
|
100
|
+
|
|
101
|
+
### Suggested fix
|
|
102
|
+
#{data["suggested_fix"]}
|
|
103
|
+
|
|
104
|
+
### Affected endpoints
|
|
105
|
+
#{Array(data["affected_endpoints"]).join(", ")}
|
|
106
|
+
|
|
107
|
+
**Regression risk:** #{data["regression_risk"]} · **Tests to run:** #{Array(data["tests_to_run"]).join(", ")}
|
|
108
|
+
MD
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.trace_md(data)
|
|
112
|
+
spans = (data["spans"] || []).map { |s| "- #{s["name"]}: #{s["duration_ms"]} ms (#{s["percent"]}%)" }
|
|
113
|
+
bottlenecks = Array(data["bottlenecks"]).map { |b| "- #{b}" }
|
|
114
|
+
<<~MD
|
|
115
|
+
## Trace: #{data["endpoint"]} (#{data["trace_id"]})
|
|
116
|
+
**Duration:** #{data["duration_ms"]} ms
|
|
117
|
+
|
|
118
|
+
### Spans
|
|
119
|
+
#{spans.join("\n")}
|
|
120
|
+
|
|
121
|
+
### Bottlenecks
|
|
122
|
+
#{bottlenecks.join("\n")}
|
|
123
|
+
MD
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.deploy_check_md(data)
|
|
127
|
+
warnings = Array(data["warnings"]).map { |w| "- #{w}" }
|
|
128
|
+
<<~MD
|
|
129
|
+
## Deploy check
|
|
130
|
+
**Ready:** #{data["ready"]} · **Last deploy:** #{data["last_deploy_at"]}
|
|
131
|
+
**New errors since deploy:** #{data["new_errors_since_deploy"]}
|
|
132
|
+
#{warnings.any? ? "**Warnings:**\n#{warnings.join("\n")}" : ""}
|
|
133
|
+
MD
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def self.doctor_md(data)
|
|
137
|
+
<<~MD
|
|
138
|
+
## Doctor
|
|
139
|
+
**Config:** #{data["config_ok"]} · **API:** #{data["api_ok"]} · **App:** #{data["app_ok"]}
|
|
140
|
+
#{data["message"]}
|
|
141
|
+
MD
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.generic_md(payload)
|
|
145
|
+
"## #{payload["command"]}\n\n```json\n#{JSON.pretty_generate(payload["data"])}\n```"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
module Human
|
|
150
|
+
def self.format(payload)
|
|
151
|
+
cmd = payload["command"]
|
|
152
|
+
data = payload["data"] || {}
|
|
153
|
+
case cmd
|
|
154
|
+
when "apps" then apps_human(data)
|
|
155
|
+
when "status" then status_human(data)
|
|
156
|
+
when "incidents" then incidents_human(data)
|
|
157
|
+
when "incident_detail" then incident_detail_human(data)
|
|
158
|
+
when "explain" then explain_human(data)
|
|
159
|
+
when "trace" then trace_human(data)
|
|
160
|
+
when "deploy_check" then deploy_check_human(data)
|
|
161
|
+
when "doctor" then doctor_human(data)
|
|
162
|
+
when "login" then "Logged in. Config saved to #{payload["config_path"]}"
|
|
163
|
+
else JSON.pretty_generate(data)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def self.apps_human(data)
|
|
168
|
+
list = data["apps"] || []
|
|
169
|
+
return "No apps found." if list.empty?
|
|
170
|
+
|
|
171
|
+
lines = [UI.c("Apps:", :bold), ""]
|
|
172
|
+
list.each do |app|
|
|
173
|
+
env_badge = app["environment"] == "production" ? UI.c("prod", :green) : UI.c(app["environment"], :gray)
|
|
174
|
+
errors = app["error_count_24h"].to_i > 0 ? UI.c("#{app["error_count_24h"]} errors", :red) : UI.c("0 errors", :gray)
|
|
175
|
+
lines << " #{UI.c(app["slug"], :cyan)} #{app["name"]} #{env_badge} #{errors}"
|
|
176
|
+
end
|
|
177
|
+
lines << ""
|
|
178
|
+
lines << "Use: activerabbit use-app <slug>"
|
|
179
|
+
lines.join("\n")
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def self.status_human(data)
|
|
183
|
+
top = data["top_issue"]
|
|
184
|
+
health_color = data["health"] == "ok" ? :green : :red
|
|
185
|
+
lines = [
|
|
186
|
+
"#{UI.c(data["name"], :bold)} (#{data["app"]})",
|
|
187
|
+
"",
|
|
188
|
+
"Health: #{UI.c(data["health"], health_color)} Errors (24h): #{data["error_count_24h"]} p95: #{data["p95_latency_ms"]} ms",
|
|
189
|
+
"Deploy: #{data["deploy_status"]} Last: #{UI.relative_time(data["last_deploy_at"])}"
|
|
190
|
+
]
|
|
191
|
+
if top
|
|
192
|
+
lines << ""
|
|
193
|
+
lines << UI.c("Top issue:", :bold)
|
|
194
|
+
lines << " #{UI.severity(top["severity"])} #{top["title"]}"
|
|
195
|
+
lines << " #{top["count"]} events · #{top["id"]}"
|
|
196
|
+
end
|
|
197
|
+
lines.join("\n")
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def self.incidents_human(data)
|
|
201
|
+
list = data["incidents"] || []
|
|
202
|
+
return "No incidents found." if list.empty?
|
|
203
|
+
|
|
204
|
+
lines = []
|
|
205
|
+
lines << ""
|
|
206
|
+
list.each_with_index do |inc, idx|
|
|
207
|
+
severity = UI.severity(inc["severity"])
|
|
208
|
+
title = UI.truncate(inc["title"], 55)
|
|
209
|
+
time_ago = UI.relative_time(inc["last_seen_at"])
|
|
210
|
+
count = inc["count"].to_s.rjust(5)
|
|
211
|
+
status_icon = case inc["status"]
|
|
212
|
+
when "wip" then UI.c("◐", :yellow)
|
|
213
|
+
when "closed" then UI.c("✓", :green)
|
|
214
|
+
else UI.c("●", :red)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
lines << "#{status_icon} #{severity} #{UI.c(inc["id"], :cyan)}"
|
|
218
|
+
lines << " #{title}"
|
|
219
|
+
lines << " #{UI.c(inc["endpoint"], :gray)} #{UI.c(count, :bold)} events #{UI.c(time_ago, :gray)}"
|
|
220
|
+
lines << "" if idx < list.size - 1
|
|
221
|
+
end
|
|
222
|
+
lines << ""
|
|
223
|
+
lines << UI.c("Use: activerabbit show <id> or activerabbit explain <id>", :gray)
|
|
224
|
+
lines.join("\n")
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def self.incident_detail_human(data)
|
|
228
|
+
severity = UI.severity(data["severity"])
|
|
229
|
+
status_color = { "open" => :red, "wip" => :yellow, "closed" => :green }[data["status"]] || :gray
|
|
230
|
+
|
|
231
|
+
lines = []
|
|
232
|
+
lines << UI.box("#{data["exception_class"]}", [
|
|
233
|
+
"#{UI.c(data["message"], :bold)}",
|
|
234
|
+
"",
|
|
235
|
+
"#{severity} #{UI.c(data["status"].upcase, status_color)} #{UI.c(data["id"], :cyan)}",
|
|
236
|
+
"",
|
|
237
|
+
"#{UI.c("Endpoint:", :gray)} #{data["endpoint"]}",
|
|
238
|
+
"#{UI.c("Count:", :gray)} #{data["count"]} #{UI.c("Affected users:", :gray)} #{data["affected_users"]}",
|
|
239
|
+
"#{UI.c("First:", :gray)} #{UI.relative_time(data["first_seen_at"])} #{UI.c("Last:", :gray)} #{UI.relative_time(data["last_seen_at"])}"
|
|
240
|
+
])
|
|
241
|
+
|
|
242
|
+
lines << ""
|
|
243
|
+
lines << UI.c("Stack trace:", :bold)
|
|
244
|
+
Array(data["backtrace"]).first(8).each do |frame|
|
|
245
|
+
if frame.include?("app/")
|
|
246
|
+
lines << " #{UI.c(frame, :cyan)}"
|
|
247
|
+
else
|
|
248
|
+
lines << " #{UI.c(frame, :gray)}"
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
if Array(data["backtrace"]).size > 8
|
|
252
|
+
lines << " #{UI.c("... #{data["backtrace"].size - 8} more frames", :gray)}"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
if data["recent_events"]&.any?
|
|
256
|
+
lines << ""
|
|
257
|
+
lines << UI.c("Recent events:", :bold)
|
|
258
|
+
data["recent_events"].first(5).each do |evt|
|
|
259
|
+
lines << " #{UI.relative_time(evt["at"])} user:#{evt["user_id"]} req:#{evt["request_id"]}"
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
lines << ""
|
|
264
|
+
lines << UI.c("Run: activerabbit explain #{data["id"]}", :gray)
|
|
265
|
+
lines.join("\n")
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def self.explain_human(data)
|
|
269
|
+
lines = [
|
|
270
|
+
UI.c("#{data["title"]} (#{data["incident_id"]})", :bold),
|
|
271
|
+
"#{UI.severity(data["severity"])} Confidence: #{(data["confidence_score"] * 100).to_i}%",
|
|
272
|
+
"",
|
|
273
|
+
UI.c("Root cause:", :bold),
|
|
274
|
+
" #{data["root_cause"]}",
|
|
275
|
+
"",
|
|
276
|
+
UI.c("Suggested fix:", :bold),
|
|
277
|
+
" #{data["suggested_fix"]}",
|
|
278
|
+
"",
|
|
279
|
+
"#{UI.c("Affected:", :gray)} #{Array(data["affected_endpoints"]).join(", ")}",
|
|
280
|
+
"#{UI.c("Regression risk:", :gray)} #{data["regression_risk"]}",
|
|
281
|
+
"#{UI.c("Tests to run:", :gray)} #{Array(data["tests_to_run"]).join(", ")}"
|
|
282
|
+
]
|
|
283
|
+
lines.join("\n")
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def self.trace_human(data)
|
|
287
|
+
lines = [
|
|
288
|
+
UI.c("Trace: #{data["endpoint"]}", :bold) + " (#{data["trace_id"]})",
|
|
289
|
+
"Duration: #{data["duration_ms"]} ms",
|
|
290
|
+
"",
|
|
291
|
+
UI.c("Spans:", :bold)
|
|
292
|
+
]
|
|
293
|
+
(data["spans"] || []).each do |s|
|
|
294
|
+
bar_len = [s["percent"].to_i / 5, 1].max
|
|
295
|
+
bar = UI.c("█" * bar_len, :cyan) + UI.c("░" * (20 - bar_len), :gray)
|
|
296
|
+
lines << " #{bar} #{s["duration_ms"].to_s.rjust(5)} ms #{s["name"]}"
|
|
297
|
+
end
|
|
298
|
+
lines << ""
|
|
299
|
+
lines << UI.c("Bottlenecks:", :bold)
|
|
300
|
+
Array(data["bottlenecks"]).each { |b| lines << " #{UI.c("·", :red)} #{b}" }
|
|
301
|
+
lines.join("\n")
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def self.deploy_check_human(data)
|
|
305
|
+
ready_icon = data["ready"] ? UI.c("✓", :green) : UI.c("✗", :red)
|
|
306
|
+
lines = [
|
|
307
|
+
"Deploy check: #{ready_icon} #{data["ready"] ? "ready" : "not ready"}",
|
|
308
|
+
"Last deploy: #{UI.relative_time(data["last_deploy_at"])}",
|
|
309
|
+
"New errors since deploy: #{data["new_errors_since_deploy"]}"
|
|
310
|
+
]
|
|
311
|
+
Array(data["warnings"]).each { |w| lines << " #{UI.c("⚠", :yellow)} #{w}" }
|
|
312
|
+
lines.join("\n")
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def self.doctor_human(data)
|
|
316
|
+
cfg_icon = data["config_ok"] ? UI.c("✓", :green) : UI.c("✗", :red)
|
|
317
|
+
api_icon = data["api_ok"] ? UI.c("✓", :green) : UI.c("✗", :red)
|
|
318
|
+
app_icon = data["app_ok"] ? UI.c("✓", :green) : UI.c("✗", :red)
|
|
319
|
+
[
|
|
320
|
+
"Config: #{cfg_icon} API: #{api_icon} App: #{app_icon}",
|
|
321
|
+
"",
|
|
322
|
+
data["message"]
|
|
323
|
+
].join("\n")
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveRabbit
|
|
4
|
+
# Terminal UI helpers: colors, relative time, box drawing
|
|
5
|
+
module UI
|
|
6
|
+
COLORS = {
|
|
7
|
+
red: "\e[31m",
|
|
8
|
+
green: "\e[32m",
|
|
9
|
+
yellow: "\e[33m",
|
|
10
|
+
blue: "\e[34m",
|
|
11
|
+
magenta: "\e[35m",
|
|
12
|
+
cyan: "\e[36m",
|
|
13
|
+
gray: "\e[90m",
|
|
14
|
+
bold: "\e[1m",
|
|
15
|
+
reset: "\e[0m"
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
SEVERITY_COLORS = {
|
|
19
|
+
"critical" => :red,
|
|
20
|
+
"high" => :red,
|
|
21
|
+
"medium" => :yellow,
|
|
22
|
+
"low" => :cyan,
|
|
23
|
+
"info" => :gray
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def color_enabled?
|
|
28
|
+
return @color_enabled if defined?(@color_enabled)
|
|
29
|
+
|
|
30
|
+
@color_enabled = $stdout.tty? && ENV["NO_COLOR"].nil?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def color_enabled=(val)
|
|
34
|
+
@color_enabled = val
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def c(text, *styles)
|
|
38
|
+
return text.to_s unless color_enabled?
|
|
39
|
+
|
|
40
|
+
codes = styles.map { |s| COLORS[s] }.compact.join
|
|
41
|
+
"#{codes}#{text}#{COLORS[:reset]}"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def severity(level)
|
|
45
|
+
color = SEVERITY_COLORS[level.to_s.downcase] || :gray
|
|
46
|
+
padded = level.to_s.upcase.ljust(8)
|
|
47
|
+
c(padded, color, :bold)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def relative_time(iso_time)
|
|
51
|
+
return "—" if iso_time.nil? || iso_time.to_s.strip == ""
|
|
52
|
+
|
|
53
|
+
begin
|
|
54
|
+
t = Time.parse(iso_time.to_s)
|
|
55
|
+
rescue ArgumentError
|
|
56
|
+
return iso_time.to_s[0, 16]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
diff = Time.now - t
|
|
60
|
+
if diff < 60
|
|
61
|
+
"just now"
|
|
62
|
+
elsif diff < 3600
|
|
63
|
+
"#{(diff / 60).to_i}m ago"
|
|
64
|
+
elsif diff < 86400
|
|
65
|
+
"#{(diff / 3600).to_i}h ago"
|
|
66
|
+
elsif diff < 604800
|
|
67
|
+
"#{(diff / 86400).to_i}d ago"
|
|
68
|
+
else
|
|
69
|
+
t.strftime("%b %d")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def truncate(str, len)
|
|
74
|
+
s = str.to_s
|
|
75
|
+
s.length > len ? "#{s[0, len - 1]}…" : s
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def box(title, lines)
|
|
79
|
+
width = [title.length + 4, lines.map { |l| strip_ansi(l).length }.max || 40].max + 2
|
|
80
|
+
bar = "─" * width
|
|
81
|
+
out = []
|
|
82
|
+
out << "┌#{bar}┐"
|
|
83
|
+
out << "│ #{c(title, :bold)}#{' ' * (width - title.length - 1)}│"
|
|
84
|
+
out << "├#{bar}┤"
|
|
85
|
+
lines.each do |line|
|
|
86
|
+
plain_len = strip_ansi(line).length
|
|
87
|
+
padding = width - plain_len - 1
|
|
88
|
+
out << "│ #{line}#{' ' * [padding, 0].max}│"
|
|
89
|
+
end
|
|
90
|
+
out << "└#{bar}┘"
|
|
91
|
+
out.join("\n")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def strip_ansi(str)
|
|
95
|
+
str.to_s.gsub(/\e\[[0-9;]*m/, "")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def prompt_choice(message, options)
|
|
99
|
+
$stderr.puts message
|
|
100
|
+
options.each_with_index do |opt, idx|
|
|
101
|
+
$stderr.puts " #{c("#{idx + 1})", :cyan)} #{opt}"
|
|
102
|
+
end
|
|
103
|
+
$stderr.print c("Choice [1-#{options.size}]: ", :bold)
|
|
104
|
+
input = $stdin.gets&.to_s&.strip
|
|
105
|
+
idx = input.to_i - 1
|
|
106
|
+
return nil if idx < 0 || idx >= options.size
|
|
107
|
+
|
|
108
|
+
options[idx]
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
data/lib/activerabbit.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: activerabbit-cli
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ActiveRabbit
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-02-25 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: minitest
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '5.0'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '5.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '13.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '13.0'
|
|
41
|
+
description: 'Production CLI for ActiveRabbit: status, incidents, explain, trace.
|
|
42
|
+
Human and machine-readable output (JSON, Markdown). Designed for developers and
|
|
43
|
+
AI agents.'
|
|
44
|
+
email:
|
|
45
|
+
- support@activerabbit.ai
|
|
46
|
+
executables:
|
|
47
|
+
- activerabbit
|
|
48
|
+
extensions: []
|
|
49
|
+
extra_rdoc_files: []
|
|
50
|
+
files:
|
|
51
|
+
- CHANGELOG.md
|
|
52
|
+
- LICENSE.txt
|
|
53
|
+
- README.md
|
|
54
|
+
- exe/activerabbit
|
|
55
|
+
- lib/activerabbit.rb
|
|
56
|
+
- lib/activerabbit/api_client.rb
|
|
57
|
+
- lib/activerabbit/cli.rb
|
|
58
|
+
- lib/activerabbit/commands.rb
|
|
59
|
+
- lib/activerabbit/config.rb
|
|
60
|
+
- lib/activerabbit/formatters.rb
|
|
61
|
+
- lib/activerabbit/ui.rb
|
|
62
|
+
- lib/activerabbit/version.rb
|
|
63
|
+
homepage: https://activerabbit.ai
|
|
64
|
+
licenses:
|
|
65
|
+
- MIT
|
|
66
|
+
metadata:
|
|
67
|
+
homepage_uri: https://activerabbit.ai
|
|
68
|
+
source_code_uri: https://github.com/activerabbit/activerabbit-cli
|
|
69
|
+
changelog_uri: https://github.com/activerabbit/activerabbit-cli/blob/main/CHANGELOG.md
|
|
70
|
+
rubygems_mfa_required: 'true'
|
|
71
|
+
post_install_message:
|
|
72
|
+
rdoc_options: []
|
|
73
|
+
require_paths:
|
|
74
|
+
- lib
|
|
75
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - ">="
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: 3.0.0
|
|
80
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
81
|
+
requirements:
|
|
82
|
+
- - ">="
|
|
83
|
+
- !ruby/object:Gem::Version
|
|
84
|
+
version: '0'
|
|
85
|
+
requirements: []
|
|
86
|
+
rubygems_version: 3.5.15
|
|
87
|
+
signing_key:
|
|
88
|
+
specification_version: 4
|
|
89
|
+
summary: CLI for ActiveRabbit.ai — monitoring and issue analysis
|
|
90
|
+
test_files: []
|