fabulous 0.1.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/LICENSE.txt +21 -0
- data/README.md +422 -0
- data/bin/fabulous +7 -0
- data/lib/fabulous/cli.rb +666 -0
- data/lib/fabulous/client.rb +89 -0
- data/lib/fabulous/configuration.rb +17 -0
- data/lib/fabulous/errors.rb +38 -0
- data/lib/fabulous/resources/base.rb +44 -0
- data/lib/fabulous/resources/dns.rb +185 -0
- data/lib/fabulous/resources/domain.rb +103 -0
- data/lib/fabulous/response.rb +226 -0
- data/lib/fabulous/version.rb +5 -0
- data/lib/fabulous.rb +33 -0
- metadata +285 -0
data/lib/fabulous/cli.rb
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require "tty-table"
|
|
5
|
+
require "tty-spinner"
|
|
6
|
+
require "tty-prompt"
|
|
7
|
+
require "pastel"
|
|
8
|
+
require "dotenv/load"
|
|
9
|
+
require "date"
|
|
10
|
+
require "fabulous"
|
|
11
|
+
|
|
12
|
+
module Fabulous
|
|
13
|
+
class Nameservers < Thor
|
|
14
|
+
def initialize(*args)
|
|
15
|
+
super
|
|
16
|
+
@pastel = Pastel.new
|
|
17
|
+
configure_client
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc "get DOMAIN", "Get nameservers for a domain"
|
|
21
|
+
def get(domain_name)
|
|
22
|
+
spinner = TTY::Spinner.new("#{@pastel.cyan('⚡')} Fetching nameservers... ", format: :dots)
|
|
23
|
+
spinner.auto_spin
|
|
24
|
+
|
|
25
|
+
begin
|
|
26
|
+
nameservers = client.domains.get_nameservers(domain_name)
|
|
27
|
+
spinner.success(@pastel.green("✓ Found nameservers"))
|
|
28
|
+
|
|
29
|
+
puts
|
|
30
|
+
puts @pastel.bold.cyan("Nameservers for #{domain_name}:")
|
|
31
|
+
if nameservers && nameservers.any?
|
|
32
|
+
nameservers.each_with_index do |ns, i|
|
|
33
|
+
puts " #{@pastel.dim("#{i + 1}.")} #{@pastel.white(ns)}"
|
|
34
|
+
end
|
|
35
|
+
else
|
|
36
|
+
puts @pastel.yellow(" No nameservers found")
|
|
37
|
+
end
|
|
38
|
+
rescue Fabulous::Error => e
|
|
39
|
+
spinner.error(@pastel.red("✗ Error: #{e.message}"))
|
|
40
|
+
exit 1
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
desc "set DOMAIN NS1 NS2 [NS3...]", "Set nameservers for a domain"
|
|
45
|
+
def set(domain_name, *nameservers)
|
|
46
|
+
if nameservers.length < 2
|
|
47
|
+
puts @pastel.red("✗ Error: At least 2 nameservers required")
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
puts @pastel.cyan("Setting nameservers for #{domain_name}:")
|
|
52
|
+
nameservers.each_with_index do |ns, i|
|
|
53
|
+
puts " #{i + 1}. #{ns}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
spinner = TTY::Spinner.new("#{@pastel.cyan('⚡')} Updating... ", format: :dots)
|
|
57
|
+
spinner.auto_spin
|
|
58
|
+
|
|
59
|
+
begin
|
|
60
|
+
if client.domains.set_nameservers(domain_name, nameservers)
|
|
61
|
+
spinner.success(@pastel.green("✓ Nameservers updated successfully"))
|
|
62
|
+
else
|
|
63
|
+
spinner.error(@pastel.red("✗ Failed to update nameservers"))
|
|
64
|
+
end
|
|
65
|
+
rescue Fabulous::Error => e
|
|
66
|
+
spinner.error(@pastel.red("✗ Error: #{e.message}"))
|
|
67
|
+
exit 1
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def configure_client
|
|
74
|
+
Fabulous.configure do |config|
|
|
75
|
+
config.username = ENV["FABULOUS_USERNAME"]
|
|
76
|
+
config.password = ENV["FABULOUS_PASSWORD"]
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def client
|
|
81
|
+
@client ||= Fabulous.client
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
class DNS < Thor
|
|
86
|
+
def initialize(*args)
|
|
87
|
+
super
|
|
88
|
+
@pastel = Pastel.new
|
|
89
|
+
@prompt = TTY::Prompt.new
|
|
90
|
+
configure_client
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
desc "list DOMAIN", "List all DNS records for a domain"
|
|
94
|
+
option :type, type: :string, enum: ["A", "AAAA", "CNAME", "MX", "TXT"], desc: "Filter by record type"
|
|
95
|
+
def list(domain_name)
|
|
96
|
+
spinner = TTY::Spinner.new("#{@pastel.cyan('⚡')} Fetching DNS records... ", format: :dots)
|
|
97
|
+
spinner.auto_spin
|
|
98
|
+
|
|
99
|
+
begin
|
|
100
|
+
records = client.dns.list_records(domain_name, type: options[:type])
|
|
101
|
+
spinner.success(@pastel.green("✓ Found #{records.length} records"))
|
|
102
|
+
|
|
103
|
+
if records.empty?
|
|
104
|
+
puts @pastel.yellow("No DNS records found")
|
|
105
|
+
else
|
|
106
|
+
display_dns_records(records)
|
|
107
|
+
end
|
|
108
|
+
rescue Fabulous::Error => e
|
|
109
|
+
spinner.error(@pastel.red("✗ Error: #{e.message}"))
|
|
110
|
+
exit 1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
desc "add DOMAIN", "Add a DNS record interactively"
|
|
115
|
+
def add(domain_name)
|
|
116
|
+
type = @prompt.select("Choose record type:", %w[A AAAA CNAME MX TXT])
|
|
117
|
+
|
|
118
|
+
case type
|
|
119
|
+
when "A"
|
|
120
|
+
hostname = @prompt.ask("Hostname (e.g., www or @ for root):")
|
|
121
|
+
ip = @prompt.ask("IP Address:")
|
|
122
|
+
ttl = @prompt.ask("TTL:", default: "3600").to_i
|
|
123
|
+
|
|
124
|
+
spinner = TTY::Spinner.new("#{@pastel.cyan('⚡')} Adding A record... ", format: :dots)
|
|
125
|
+
spinner.auto_spin
|
|
126
|
+
|
|
127
|
+
if client.dns.add_a_record(domain_name, hostname: hostname, ip_address: ip, ttl: ttl)
|
|
128
|
+
spinner.success(@pastel.green("✓ A record added"))
|
|
129
|
+
else
|
|
130
|
+
spinner.error(@pastel.red("✗ Failed to add record"))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
when "MX"
|
|
134
|
+
hostname = @prompt.ask("Mail server hostname:")
|
|
135
|
+
priority = @prompt.ask("Priority:", default: "10").to_i
|
|
136
|
+
ttl = @prompt.ask("TTL:", default: "3600").to_i
|
|
137
|
+
|
|
138
|
+
spinner = TTY::Spinner.new("#{@pastel.cyan('⚡')} Adding MX record... ", format: :dots)
|
|
139
|
+
spinner.auto_spin
|
|
140
|
+
|
|
141
|
+
if client.dns.add_mx_record(domain_name, hostname: hostname, priority: priority, ttl: ttl)
|
|
142
|
+
spinner.success(@pastel.green("✓ MX record added"))
|
|
143
|
+
else
|
|
144
|
+
spinner.error(@pastel.red("✗ Failed to add record"))
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
when "CNAME"
|
|
148
|
+
alias_name = @prompt.ask("Alias (e.g., blog):")
|
|
149
|
+
target = @prompt.ask("Target domain:")
|
|
150
|
+
ttl = @prompt.ask("TTL:", default: "3600").to_i
|
|
151
|
+
|
|
152
|
+
spinner = TTY::Spinner.new("#{@pastel.cyan('⚡')} Adding CNAME record... ", format: :dots)
|
|
153
|
+
spinner.auto_spin
|
|
154
|
+
|
|
155
|
+
if client.dns.add_cname_record(domain_name, alias_name: alias_name, target: target, ttl: ttl)
|
|
156
|
+
spinner.success(@pastel.green("✓ CNAME record added"))
|
|
157
|
+
else
|
|
158
|
+
spinner.error(@pastel.red("✗ Failed to add record"))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
when "TXT"
|
|
162
|
+
hostname = @prompt.ask("Hostname (@ for root):")
|
|
163
|
+
text = @prompt.ask("Text value:")
|
|
164
|
+
ttl = @prompt.ask("TTL:", default: "3600").to_i
|
|
165
|
+
|
|
166
|
+
spinner = TTY::Spinner.new("#{@pastel.cyan('⚡')} Adding TXT record... ", format: :dots)
|
|
167
|
+
spinner.auto_spin
|
|
168
|
+
|
|
169
|
+
if client.dns.add_txt_record(domain_name, hostname: hostname, text: text, ttl: ttl)
|
|
170
|
+
spinner.success(@pastel.green("✓ TXT record added"))
|
|
171
|
+
else
|
|
172
|
+
spinner.error(@pastel.red("✗ Failed to add record"))
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
rescue Fabulous::Error => e
|
|
176
|
+
puts @pastel.red("✗ Error: #{e.message}")
|
|
177
|
+
exit 1
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def configure_client
|
|
183
|
+
Fabulous.configure do |config|
|
|
184
|
+
config.username = ENV["FABULOUS_USERNAME"]
|
|
185
|
+
config.password = ENV["FABULOUS_PASSWORD"]
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def client
|
|
190
|
+
@client ||= Fabulous.client
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def display_dns_records(records)
|
|
194
|
+
table = TTY::Table.new(
|
|
195
|
+
header: [
|
|
196
|
+
@pastel.bold("Type"),
|
|
197
|
+
@pastel.bold("Name"),
|
|
198
|
+
@pastel.bold("Value"),
|
|
199
|
+
@pastel.bold("TTL"),
|
|
200
|
+
@pastel.bold("Priority")
|
|
201
|
+
]
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
records.each do |record|
|
|
205
|
+
table << [
|
|
206
|
+
dns_type_badge(record[:type]),
|
|
207
|
+
record[:name] || "-",
|
|
208
|
+
truncate(record[:value], 40),
|
|
209
|
+
record[:ttl] || "-",
|
|
210
|
+
record[:priority] || "-"
|
|
211
|
+
]
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
puts
|
|
215
|
+
puts table.render(:unicode, padding: [0, 1], border: { style: :cyan })
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def dns_type_badge(type)
|
|
219
|
+
colors = {
|
|
220
|
+
"A" => :green,
|
|
221
|
+
"AAAA" => :green,
|
|
222
|
+
"CNAME" => :yellow,
|
|
223
|
+
"MX" => :blue,
|
|
224
|
+
"TXT" => :magenta
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
color = colors[type] || :white
|
|
228
|
+
@pastel.send(color, type || "?")
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def truncate(text, length)
|
|
232
|
+
return "-" unless text
|
|
233
|
+
text.length > length ? "#{text[0...length]}..." : text
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
class CLI < Thor
|
|
238
|
+
def self.exit_on_failure?
|
|
239
|
+
true
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def initialize(*args)
|
|
243
|
+
super
|
|
244
|
+
@pastel = Pastel.new
|
|
245
|
+
@prompt = TTY::Prompt.new
|
|
246
|
+
configure_client
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
desc "list", "List all domains in your portfolio"
|
|
250
|
+
option :sort, type: :string, enum: ["name", "expiry", "status"], default: "name", desc: "Sort by field"
|
|
251
|
+
option :filter, type: :string, desc: "Filter domains by name (partial match)"
|
|
252
|
+
option :expiring, type: :numeric, desc: "Show domains expiring within N days"
|
|
253
|
+
option :page, type: :numeric, desc: "Show specific page (without pagination)"
|
|
254
|
+
option :limit, type: :numeric, default: 20, desc: "Number of domains per page"
|
|
255
|
+
option :interactive, type: :boolean, default: false, desc: "Enable interactive pagination"
|
|
256
|
+
def list
|
|
257
|
+
spinner = TTY::Spinner.new("#{@pastel.cyan('⚡')} Fetching domains... ", format: :dots)
|
|
258
|
+
spinner.auto_spin
|
|
259
|
+
|
|
260
|
+
begin
|
|
261
|
+
domains = if options[:page]
|
|
262
|
+
client.domains.list(page: options[:page])
|
|
263
|
+
else
|
|
264
|
+
client.domains.all
|
|
265
|
+
end
|
|
266
|
+
spinner.success(@pastel.green("✓ Found #{domains.length} domains"))
|
|
267
|
+
|
|
268
|
+
# Apply filters
|
|
269
|
+
if options[:filter]
|
|
270
|
+
domains = domains.select { |d| d[:name].include?(options[:filter]) }
|
|
271
|
+
puts @pastel.yellow("Filtered to #{domains.length} domains matching '#{options[:filter]}'")
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
if options[:expiring]
|
|
275
|
+
today = Date.today
|
|
276
|
+
domains = domains.select do |d|
|
|
277
|
+
next false unless d[:expiry_date]
|
|
278
|
+
begin
|
|
279
|
+
expiry = Date.parse(d[:expiry_date])
|
|
280
|
+
days = (expiry - today).to_i
|
|
281
|
+
days > 0 && days <= options[:expiring]
|
|
282
|
+
rescue ArgumentError
|
|
283
|
+
false
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
puts @pastel.yellow("Showing #{domains.length} domains expiring within #{options[:expiring]} days")
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Sort domains
|
|
290
|
+
domains = sort_domains(domains, options[:sort])
|
|
291
|
+
|
|
292
|
+
# Display domains
|
|
293
|
+
if domains.empty?
|
|
294
|
+
puts @pastel.yellow("No domains found matching your criteria")
|
|
295
|
+
else
|
|
296
|
+
display_domains_table(domains, options[:limit], options[:interactive])
|
|
297
|
+
end
|
|
298
|
+
rescue Fabulous::Error => e
|
|
299
|
+
spinner.error(@pastel.red("✗ Error: #{e.message}"))
|
|
300
|
+
exit 1
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
desc "info DOMAIN", "Show detailed information about a domain"
|
|
305
|
+
def info(domain_name)
|
|
306
|
+
spinner = TTY::Spinner.new("#{@pastel.cyan('⚡')} Fetching domain info... ", format: :dots)
|
|
307
|
+
spinner.auto_spin
|
|
308
|
+
|
|
309
|
+
begin
|
|
310
|
+
info = client.domains.info(domain_name)
|
|
311
|
+
|
|
312
|
+
if info.nil?
|
|
313
|
+
spinner.error(@pastel.red("✗ Domain not found or no information available"))
|
|
314
|
+
exit 1
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
spinner.success(@pastel.green("✓ Domain found"))
|
|
318
|
+
|
|
319
|
+
puts
|
|
320
|
+
puts @pastel.bold.cyan("═" * 60)
|
|
321
|
+
puts @pastel.bold.white(" Domain Information: #{domain_name}")
|
|
322
|
+
puts @pastel.bold.cyan("═" * 60)
|
|
323
|
+
puts
|
|
324
|
+
|
|
325
|
+
display_info_item("Status", info[:status] || "Active", status_color(info[:status]))
|
|
326
|
+
display_info_item("Created", info[:creation_date] || "-")
|
|
327
|
+
display_info_item("Expires", info[:expiry_date] || "-", expiry_color(info[:expiry_date]))
|
|
328
|
+
display_info_item("Auto-Renew", info[:auto_renew].nil? ? "-" : (info[:auto_renew] ? "Enabled" : "Disabled"),
|
|
329
|
+
info[:auto_renew] ? :green : nil)
|
|
330
|
+
display_info_item("Domain Lock", info[:locked].nil? ? "-" : (info[:locked] ? "Locked" : "Unlocked"),
|
|
331
|
+
info[:locked] ? :green : nil)
|
|
332
|
+
display_info_item("WHOIS Privacy", info[:whois_privacy].nil? ? "-" : (info[:whois_privacy] ? "Enabled" : "Disabled"),
|
|
333
|
+
info[:whois_privacy] ? :green : nil)
|
|
334
|
+
|
|
335
|
+
if info[:nameservers] && info[:nameservers].any?
|
|
336
|
+
puts
|
|
337
|
+
puts @pastel.bold.cyan("Nameservers:")
|
|
338
|
+
info[:nameservers].each_with_index do |ns, i|
|
|
339
|
+
puts " #{@pastel.dim("#{i + 1}.")} #{@pastel.white(ns)}"
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
puts
|
|
344
|
+
puts @pastel.cyan("─" * 60)
|
|
345
|
+
rescue Fabulous::Error => e
|
|
346
|
+
spinner.error(@pastel.red("✗ Error: #{e.message}"))
|
|
347
|
+
exit 1
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
desc "search QUERY", "Search for domains"
|
|
352
|
+
def search(query)
|
|
353
|
+
invoke :list, [], filter: query
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
desc "expiring [DAYS]", "Show domains expiring soon"
|
|
357
|
+
def expiring(days = 30)
|
|
358
|
+
invoke :list, [], expiring: days.to_i
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
desc "nameservers SUBCOMMAND ...ARGS", "Manage nameservers"
|
|
362
|
+
subcommand "nameservers", Nameservers
|
|
363
|
+
|
|
364
|
+
desc "dns SUBCOMMAND ...ARGS", "Manage DNS records"
|
|
365
|
+
subcommand "dns", DNS
|
|
366
|
+
|
|
367
|
+
desc "check DOMAIN", "Check if a domain is available"
|
|
368
|
+
def check(domain_name)
|
|
369
|
+
spinner = TTY::Spinner.new("#{@pastel.cyan('⚡')} Checking availability... ", format: :dots)
|
|
370
|
+
spinner.auto_spin
|
|
371
|
+
|
|
372
|
+
begin
|
|
373
|
+
available = client.domains.check(domain_name)
|
|
374
|
+
|
|
375
|
+
if available
|
|
376
|
+
spinner.success(@pastel.green("✓ #{domain_name} is available!"))
|
|
377
|
+
else
|
|
378
|
+
spinner.stop(@pastel.yellow("✗ #{domain_name} is not available"))
|
|
379
|
+
end
|
|
380
|
+
rescue Fabulous::Error => e
|
|
381
|
+
spinner.error(@pastel.red("✗ Error: #{e.message}"))
|
|
382
|
+
exit 1
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
desc "summary", "Show portfolio summary"
|
|
387
|
+
def summary
|
|
388
|
+
spinner = TTY::Spinner.new("#{@pastel.cyan('⚡')} Analyzing portfolio... ", format: :dots)
|
|
389
|
+
spinner.auto_spin
|
|
390
|
+
|
|
391
|
+
begin
|
|
392
|
+
domains = client.domains.all
|
|
393
|
+
spinner.success(@pastel.green("✓ Analysis complete"))
|
|
394
|
+
|
|
395
|
+
puts
|
|
396
|
+
puts @pastel.bold.cyan("═" * 60)
|
|
397
|
+
puts @pastel.bold.white(" Portfolio Summary")
|
|
398
|
+
puts @pastel.bold.cyan("═" * 60)
|
|
399
|
+
puts
|
|
400
|
+
|
|
401
|
+
# Total domains
|
|
402
|
+
puts "#{@pastel.bold('Total Domains:')} #{@pastel.cyan(domains.length.to_s)}"
|
|
403
|
+
puts
|
|
404
|
+
|
|
405
|
+
# Group by year
|
|
406
|
+
by_year = domains.group_by do |d|
|
|
407
|
+
Date.parse(d[:expiry_date]).year rescue "Unknown"
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
puts @pastel.bold("Domains by Expiry Year:")
|
|
411
|
+
by_year.sort.each do |year, year_domains|
|
|
412
|
+
bar_length = (year_domains.length.to_f / domains.length * 30).round
|
|
413
|
+
bar = "█" * bar_length
|
|
414
|
+
puts " #{year}: #{@pastel.cyan(bar)} #{year_domains.length}"
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Expiring soon
|
|
418
|
+
today = Date.today
|
|
419
|
+
expiring_30 = domains.count do |d|
|
|
420
|
+
next false unless d[:expiry_date]
|
|
421
|
+
begin
|
|
422
|
+
expiry = Date.parse(d[:expiry_date])
|
|
423
|
+
days = (expiry - today).to_i
|
|
424
|
+
days > 0 && days <= 30
|
|
425
|
+
rescue
|
|
426
|
+
false
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
expiring_90 = domains.count do |d|
|
|
431
|
+
next false unless d[:expiry_date]
|
|
432
|
+
begin
|
|
433
|
+
expiry = Date.parse(d[:expiry_date])
|
|
434
|
+
days = (expiry - today).to_i
|
|
435
|
+
days > 0 && days <= 90
|
|
436
|
+
rescue
|
|
437
|
+
false
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
puts
|
|
442
|
+
puts @pastel.bold("Expiring Soon:")
|
|
443
|
+
puts " Next 30 days: #{color_expiry_count(expiring_30)}"
|
|
444
|
+
puts " Next 90 days: #{color_expiry_count(expiring_90)}"
|
|
445
|
+
|
|
446
|
+
puts
|
|
447
|
+
puts @pastel.cyan("─" * 60)
|
|
448
|
+
rescue Fabulous::Error => e
|
|
449
|
+
spinner.error(@pastel.red("✗ Error: #{e.message}"))
|
|
450
|
+
exit 1
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
desc "version", "Show version"
|
|
455
|
+
def version
|
|
456
|
+
puts "Fabulous CLI v#{Fabulous::VERSION}"
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
private
|
|
460
|
+
|
|
461
|
+
def configure_client
|
|
462
|
+
Fabulous.configure do |config|
|
|
463
|
+
config.username = ENV["FABULOUS_USERNAME"]
|
|
464
|
+
config.password = ENV["FABULOUS_PASSWORD"]
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
unless Fabulous.configuration.valid?
|
|
468
|
+
puts @pastel.red("✗ Error: Missing credentials")
|
|
469
|
+
puts "Please set FABULOUS_USERNAME and FABULOUS_PASSWORD environment variables"
|
|
470
|
+
puts "You can create a .env file with:"
|
|
471
|
+
puts " FABULOUS_USERNAME=your_username"
|
|
472
|
+
puts " FABULOUS_PASSWORD=your_password"
|
|
473
|
+
exit 1
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
def client
|
|
478
|
+
@client ||= Fabulous.client
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def sort_domains(domains, sort_by)
|
|
482
|
+
case sort_by
|
|
483
|
+
when "expiry"
|
|
484
|
+
domains.sort_by { |d| d[:expiry_date] || "9999-12-31" }
|
|
485
|
+
when "status"
|
|
486
|
+
domains.sort_by { |d| d[:status] || "Unknown" }
|
|
487
|
+
else # name
|
|
488
|
+
domains.sort_by { |d| d[:name] }
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def display_domains_table(domains, limit, interactive = false)
|
|
493
|
+
if interactive
|
|
494
|
+
display_domains_interactive(domains, limit)
|
|
495
|
+
else
|
|
496
|
+
# Non-interactive display - show all or limited number
|
|
497
|
+
display_limit = limit || domains.length
|
|
498
|
+
domains_to_show = domains.first(display_limit)
|
|
499
|
+
|
|
500
|
+
table = TTY::Table.new(
|
|
501
|
+
header: [
|
|
502
|
+
@pastel.bold("Domain"),
|
|
503
|
+
@pastel.bold("Status"),
|
|
504
|
+
@pastel.bold("Expires"),
|
|
505
|
+
@pastel.bold("Days Left")
|
|
506
|
+
]
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
today = Date.today
|
|
510
|
+
domains_to_show.each do |domain|
|
|
511
|
+
days_left = calculate_days_left(domain[:expiry_date])
|
|
512
|
+
|
|
513
|
+
table << [
|
|
514
|
+
@pastel.white(domain[:name]),
|
|
515
|
+
status_badge(domain[:status]),
|
|
516
|
+
domain[:expiry_date] || "-",
|
|
517
|
+
days_left_badge(days_left)
|
|
518
|
+
]
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
puts
|
|
522
|
+
puts table.render(:unicode, padding: [0, 1], border: { style: :cyan })
|
|
523
|
+
|
|
524
|
+
if domains.length > display_limit
|
|
525
|
+
puts
|
|
526
|
+
puts @pastel.dim("Showing #{display_limit} of #{domains.length} domains (use --limit to show more)")
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def display_domains_interactive(domains, limit)
|
|
532
|
+
pages = (domains.length.to_f / limit).ceil
|
|
533
|
+
current_page = 0
|
|
534
|
+
|
|
535
|
+
loop do
|
|
536
|
+
start_idx = current_page * limit
|
|
537
|
+
end_idx = start_idx + limit
|
|
538
|
+
page_domains = domains[start_idx...end_idx]
|
|
539
|
+
|
|
540
|
+
table = TTY::Table.new(
|
|
541
|
+
header: [
|
|
542
|
+
@pastel.bold("Domain"),
|
|
543
|
+
@pastel.bold("Status"),
|
|
544
|
+
@pastel.bold("Expires"),
|
|
545
|
+
@pastel.bold("Days Left")
|
|
546
|
+
]
|
|
547
|
+
)
|
|
548
|
+
|
|
549
|
+
today = Date.today
|
|
550
|
+
page_domains.each do |domain|
|
|
551
|
+
days_left = calculate_days_left(domain[:expiry_date])
|
|
552
|
+
|
|
553
|
+
table << [
|
|
554
|
+
@pastel.white(domain[:name]),
|
|
555
|
+
status_badge(domain[:status]),
|
|
556
|
+
domain[:expiry_date] || "-",
|
|
557
|
+
days_left_badge(days_left)
|
|
558
|
+
]
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
puts
|
|
562
|
+
puts table.render(:unicode, padding: [0, 1], border: { style: :cyan })
|
|
563
|
+
|
|
564
|
+
if pages > 1
|
|
565
|
+
puts
|
|
566
|
+
puts @pastel.dim("Page #{current_page + 1} of #{pages} (#{domains.length} total domains)")
|
|
567
|
+
|
|
568
|
+
choices = []
|
|
569
|
+
choices << "Next page" if current_page < pages - 1
|
|
570
|
+
choices << "Previous page" if current_page > 0
|
|
571
|
+
choices << "Exit"
|
|
572
|
+
|
|
573
|
+
choice = @prompt.select("Navigate:", choices, cycle: true)
|
|
574
|
+
|
|
575
|
+
case choice
|
|
576
|
+
when "Next page"
|
|
577
|
+
current_page += 1
|
|
578
|
+
when "Previous page"
|
|
579
|
+
current_page -= 1
|
|
580
|
+
when "Exit"
|
|
581
|
+
break
|
|
582
|
+
end
|
|
583
|
+
else
|
|
584
|
+
break
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def calculate_days_left(expiry_date)
|
|
590
|
+
return nil unless expiry_date
|
|
591
|
+
begin
|
|
592
|
+
expiry = Date.parse(expiry_date)
|
|
593
|
+
(expiry - Date.today).to_i
|
|
594
|
+
rescue ArgumentError
|
|
595
|
+
nil
|
|
596
|
+
end
|
|
597
|
+
end
|
|
598
|
+
|
|
599
|
+
def days_left_badge(days)
|
|
600
|
+
return "-" if days.nil?
|
|
601
|
+
|
|
602
|
+
if days < 0
|
|
603
|
+
@pastel.red("Expired")
|
|
604
|
+
elsif days <= 30
|
|
605
|
+
@pastel.red("#{days}d")
|
|
606
|
+
elsif days <= 90
|
|
607
|
+
@pastel.yellow("#{days}d")
|
|
608
|
+
else
|
|
609
|
+
@pastel.green("#{days}d")
|
|
610
|
+
end
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
def status_badge(status)
|
|
614
|
+
case status&.downcase
|
|
615
|
+
when "active"
|
|
616
|
+
@pastel.green("● Active")
|
|
617
|
+
when "inactive"
|
|
618
|
+
@pastel.red("● Inactive")
|
|
619
|
+
when "pending"
|
|
620
|
+
@pastel.yellow("● Pending")
|
|
621
|
+
else
|
|
622
|
+
@pastel.dim("● #{status || 'Unknown'}")
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def display_info_item(label, value, color = nil)
|
|
627
|
+
formatted_value = value || "-"
|
|
628
|
+
formatted_value = @pastel.send(color, formatted_value) if color
|
|
629
|
+
puts " #{@pastel.bold(label.ljust(15))} #{formatted_value}"
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
def status_color(status)
|
|
633
|
+
case status&.downcase
|
|
634
|
+
when "active" then :green
|
|
635
|
+
when "inactive" then :red
|
|
636
|
+
when "pending" then :yellow
|
|
637
|
+
else nil
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
|
|
641
|
+
def expiry_color(expiry_date)
|
|
642
|
+
days = calculate_days_left(expiry_date)
|
|
643
|
+
return nil unless days
|
|
644
|
+
|
|
645
|
+
if days < 0
|
|
646
|
+
:red
|
|
647
|
+
elsif days <= 30
|
|
648
|
+
:red
|
|
649
|
+
elsif days <= 90
|
|
650
|
+
:yellow
|
|
651
|
+
else
|
|
652
|
+
:green
|
|
653
|
+
end
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def color_expiry_count(count)
|
|
657
|
+
if count == 0
|
|
658
|
+
@pastel.green(count.to_s)
|
|
659
|
+
elsif count <= 5
|
|
660
|
+
@pastel.yellow(count.to_s)
|
|
661
|
+
else
|
|
662
|
+
@pastel.red(count.to_s)
|
|
663
|
+
end
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
end
|