rogiq 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/exe/rogiq +49 -0
- data/lib/rogiq/cli.rb +51 -0
- data/lib/rogiq/commands/accounts.rb +114 -0
- data/lib/rogiq/commands/ai.rb +71 -0
- data/lib/rogiq/commands/auth.rb +194 -0
- data/lib/rogiq/commands/base.rb +26 -0
- data/lib/rogiq/commands/billing.rb +90 -0
- data/lib/rogiq/commands/billing_stripe.rb +15 -0
- data/lib/rogiq/commands/clients.rb +117 -0
- data/lib/rogiq/commands/content.rb +71 -0
- data/lib/rogiq/commands/diagnose.rb +134 -0
- data/lib/rogiq/commands/jobs.rb +126 -0
- data/lib/rogiq/commands/queue.rb +115 -0
- data/lib/rogiq/commands/security.rb +33 -0
- data/lib/rogiq/commands/seo.rb +49 -0
- data/lib/rogiq/commands/seo_audit.rb +51 -0
- data/lib/rogiq/commands/status.rb +166 -0
- data/lib/rogiq/commands/sync.rb +60 -0
- data/lib/rogiq/config_store.rb +59 -0
- data/lib/rogiq/formatters.rb +103 -0
- data/lib/rogiq/helpers.rb +53 -0
- data/lib/rogiq/http_api.rb +81 -0
- data/lib/rogiq/rails_loader.rb +22 -0
- data/lib/rogiq/remote/accounts.rb +38 -0
- data/lib/rogiq/remote/ai.rb +50 -0
- data/lib/rogiq/remote/billing.rb +39 -0
- data/lib/rogiq/remote/clients.rb +44 -0
- data/lib/rogiq/remote/command_base.rb +41 -0
- data/lib/rogiq/remote/content.rb +37 -0
- data/lib/rogiq/remote/diagnose.rb +24 -0
- data/lib/rogiq/remote/jobs.rb +52 -0
- data/lib/rogiq/remote/queue.rb +72 -0
- data/lib/rogiq/remote/security.rb +26 -0
- data/lib/rogiq/remote/seo.rb +32 -0
- data/lib/rogiq/remote/status.rb +35 -0
- data/lib/rogiq/remote/sync.rb +49 -0
- data/lib/rogiq/remote_cli.rb +73 -0
- data/lib/rogiq/version.rb +5 -0
- metadata +96 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1e6164cbe6a3fe5aeec2b3602d8c2401c552cb4d08ee8160d044bdf0296e17a9
|
|
4
|
+
data.tar.gz: c80152ab3a4191dc0991f178bcc60a94f2aa359cc176e5d16989e47a078e410a
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8320cbe572bbfc5e54405e1a4e9967bfe900d0902bdf2c417c7dbb65c2aabde043fb87ed425c7e2c79d0cf9cc1e377212e3a4bacaf8a47652822cfb8020f2597
|
|
7
|
+
data.tar.gz: 61c8fb1e700c98086f848086d33c1de7046181128f6857d4b05b30183dc152dc299766ab3abff5e73559a7d12cda5b3eb7e5e81ad9c226e761b6deaa57981e5e
|
data/exe/rogiq
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
lib = File.expand_path("../lib", __dir__)
|
|
5
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
6
|
+
|
|
7
|
+
require "rogiq/version"
|
|
8
|
+
|
|
9
|
+
argv = ARGV.dup
|
|
10
|
+
if %w[version -v --version].include?(argv.first)
|
|
11
|
+
puts RogIQ::VERSION
|
|
12
|
+
exit 0
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
force_local = ENV["ROGIQ_LOCAL"].to_s == "1" || argv.delete("--local")
|
|
16
|
+
|
|
17
|
+
require "rogiq/config_store"
|
|
18
|
+
|
|
19
|
+
first = argv.first
|
|
20
|
+
auth_cmds = %w[login logout whoami use]
|
|
21
|
+
|
|
22
|
+
if auth_cmds.include?(first)
|
|
23
|
+
require "rogiq/commands/auth"
|
|
24
|
+
RogIQ::Commands::Auth.start(argv)
|
|
25
|
+
elsif !force_local && RogIQ::ConfigStore.remote_available?
|
|
26
|
+
require "rogiq/remote_cli"
|
|
27
|
+
RogIQ::RemoteCLI.start(argv)
|
|
28
|
+
else
|
|
29
|
+
require "rogiq/rails_loader"
|
|
30
|
+
require "rogiq/formatters"
|
|
31
|
+
require "rogiq/helpers"
|
|
32
|
+
require "rogiq/commands/base"
|
|
33
|
+
require "rogiq/commands/status"
|
|
34
|
+
require "rogiq/commands/queue"
|
|
35
|
+
require "rogiq/commands/jobs"
|
|
36
|
+
require "rogiq/commands/clients"
|
|
37
|
+
require "rogiq/commands/accounts"
|
|
38
|
+
require "rogiq/commands/billing_stripe"
|
|
39
|
+
require "rogiq/commands/billing"
|
|
40
|
+
require "rogiq/commands/ai"
|
|
41
|
+
require "rogiq/commands/seo_audit"
|
|
42
|
+
require "rogiq/commands/seo"
|
|
43
|
+
require "rogiq/commands/content"
|
|
44
|
+
require "rogiq/commands/diagnose"
|
|
45
|
+
require "rogiq/commands/security"
|
|
46
|
+
require "rogiq/commands/sync"
|
|
47
|
+
require "rogiq/cli"
|
|
48
|
+
RogIQ::CLI.start(argv)
|
|
49
|
+
end
|
data/lib/rogiq/cli.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module RogIQ
|
|
6
|
+
class CLI < Thor
|
|
7
|
+
package_name "RogIQ"
|
|
8
|
+
|
|
9
|
+
desc "version", "Print rogiq version"
|
|
10
|
+
map %w[--version -v] => :version
|
|
11
|
+
def version
|
|
12
|
+
puts RogIQ::VERSION
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "status SUBCOMMAND ...", "System health / queues / DB / Redis / AI config"
|
|
16
|
+
subcommand "status", RogIQ::Commands::Status
|
|
17
|
+
|
|
18
|
+
desc "queue SUBCOMMAND ...", "Solid Queue operations"
|
|
19
|
+
subcommand "queue", RogIQ::Commands::Queue
|
|
20
|
+
|
|
21
|
+
desc "jobs SUBCOMMAND ...", "Inspect and manage failed Solid Queue jobs"
|
|
22
|
+
subcommand "jobs", RogIQ::Commands::Jobs
|
|
23
|
+
|
|
24
|
+
desc "clients SUBCOMMAND ...", "Client lookup and settings"
|
|
25
|
+
subcommand "clients", RogIQ::Commands::Clients
|
|
26
|
+
|
|
27
|
+
desc "accounts SUBCOMMAND ...", "Accounts, subscriptions, usage"
|
|
28
|
+
subcommand "accounts", RogIQ::Commands::Accounts
|
|
29
|
+
|
|
30
|
+
desc "ai SUBCOMMAND ...", "AI usage, models, jobs"
|
|
31
|
+
subcommand "ai", RogIQ::Commands::AI
|
|
32
|
+
|
|
33
|
+
desc "seo SUBCOMMAND ...", "SEO module, audits, keywords"
|
|
34
|
+
subcommand "seo", RogIQ::Commands::SEO
|
|
35
|
+
|
|
36
|
+
desc "billing SUBCOMMAND ...", "Coins, invoices, Stripe catalog, usage overages"
|
|
37
|
+
subcommand "billing", RogIQ::Commands::Billing
|
|
38
|
+
|
|
39
|
+
desc "content SUBCOMMAND ...", "Content pipeline & healing"
|
|
40
|
+
subcommand "content", RogIQ::Commands::Content
|
|
41
|
+
|
|
42
|
+
desc "diagnose SUBCOMMAND ...", "Targeted diagnostics"
|
|
43
|
+
subcommand "diagnose", RogIQ::Commands::Diagnose
|
|
44
|
+
|
|
45
|
+
desc "security SUBCOMMAND ...", "Brakeman, bundler-audit, security_policy rake"
|
|
46
|
+
subcommand "security", RogIQ::Commands::Security
|
|
47
|
+
|
|
48
|
+
desc "sync SUBCOMMAND ...", "Postman, Stripe catalog, WordPress, integrations"
|
|
49
|
+
subcommand "sync", RogIQ::Commands::Sync
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RogIQ
|
|
4
|
+
module Commands
|
|
5
|
+
class Accounts < RogIQ::Commands::Base
|
|
6
|
+
desc "list", "List accounts"
|
|
7
|
+
method_option :search, type: :string, desc: "Filter name or slug (ILIKE)"
|
|
8
|
+
method_option :limit, type: :numeric, default: 40, aliases: "-n"
|
|
9
|
+
def list
|
|
10
|
+
RogIQ.load_rails!
|
|
11
|
+
scope = ::Account.order(created_at: :desc)
|
|
12
|
+
if options[:search].present?
|
|
13
|
+
q = "%#{options[:search]}%"
|
|
14
|
+
scope = scope.where("name ILIKE ? OR slug ILIKE ?", q, q)
|
|
15
|
+
end
|
|
16
|
+
rows = scope.limit(options[:limit]).map do |a|
|
|
17
|
+
[a.id, a.name, a.slug, a.status, a.account_type]
|
|
18
|
+
end
|
|
19
|
+
fmt.output(headers: %w[id name slug status account_type], rows: rows)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "show IDENTIFIER", "Account detail by id, slug, or name"
|
|
23
|
+
def show(identifier)
|
|
24
|
+
RogIQ.load_rails!
|
|
25
|
+
a = RogIQ::Helpers.resolve_account(identifier)
|
|
26
|
+
unless a
|
|
27
|
+
fmt.error_msg("Account not found")
|
|
28
|
+
exit 1
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
fmt.output(
|
|
32
|
+
id: a.id,
|
|
33
|
+
name: a.name,
|
|
34
|
+
slug: a.slug,
|
|
35
|
+
status: a.status,
|
|
36
|
+
account_type: a.account_type,
|
|
37
|
+
billing_status: a.try(:billing_status),
|
|
38
|
+
created_at: a.created_at
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
desc "usage IDENTIFIER", "Usage meters, coin wallet, open overages"
|
|
43
|
+
def usage(identifier)
|
|
44
|
+
RogIQ.load_rails!
|
|
45
|
+
a = RogIQ::Helpers.resolve_account(identifier)
|
|
46
|
+
unless a
|
|
47
|
+
fmt.error_msg("Account not found")
|
|
48
|
+
exit 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
meters = ::UsageMeter.where(account_id: a.id).order(period_end: :desc).limit(20).map do |m|
|
|
52
|
+
{
|
|
53
|
+
metric_type: m.metric_type,
|
|
54
|
+
period_start: m.period_start,
|
|
55
|
+
period_end: m.period_end,
|
|
56
|
+
current_value: m.current_value
|
|
57
|
+
}
|
|
58
|
+
end
|
|
59
|
+
wallet = a.rogue_coin_wallet
|
|
60
|
+
overages = a.usage_overages.status_pending.limit(20)
|
|
61
|
+
|
|
62
|
+
fmt.output(
|
|
63
|
+
account_id: a.id,
|
|
64
|
+
rogue_coin_balance: wallet&.balance,
|
|
65
|
+
rogue_coin_lifetime_spent: wallet&.lifetime_spent,
|
|
66
|
+
usage_meters: meters,
|
|
67
|
+
open_overages: overages.map do |o|
|
|
68
|
+
{
|
|
69
|
+
id: o.id,
|
|
70
|
+
metric: o.metric_type,
|
|
71
|
+
overage_amount: o.overage_amount,
|
|
72
|
+
total_charge: o.total_charge.to_f,
|
|
73
|
+
status: o.status,
|
|
74
|
+
created_at: o.created_at
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
desc "subscription IDENTIFIER", "Subscription + plan summary"
|
|
81
|
+
def subscription(identifier)
|
|
82
|
+
RogIQ.load_rails!
|
|
83
|
+
a = RogIQ::Helpers.resolve_account(identifier)
|
|
84
|
+
unless a
|
|
85
|
+
fmt.error_msg("Account not found")
|
|
86
|
+
exit 1
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
sub = a.subscription
|
|
90
|
+
plan = sub&.subscription_plan
|
|
91
|
+
fmt.output(
|
|
92
|
+
account_id: a.id,
|
|
93
|
+
subscription: sub&.slice(:id, :status, :trial_ends_at, :current_period_start, :current_period_end, :cancel_at_period_end),
|
|
94
|
+
plan: plan&.slice(:id, :name, :slug)
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
desc "users IDENTIFIER", "Active account users + roles"
|
|
99
|
+
def users(identifier)
|
|
100
|
+
RogIQ.load_rails!
|
|
101
|
+
a = RogIQ::Helpers.resolve_account(identifier)
|
|
102
|
+
unless a
|
|
103
|
+
fmt.error_msg("Account not found")
|
|
104
|
+
exit 1
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
rows = a.account_users.includes(:user, :role).where(status: "active").map do |au|
|
|
108
|
+
[au.user&.email, au.role&.name, au.status]
|
|
109
|
+
end
|
|
110
|
+
fmt.output(headers: %w[email role status], rows: rows)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RogIQ
|
|
4
|
+
module Commands
|
|
5
|
+
class AI < RogIQ::Commands::Base
|
|
6
|
+
desc "status", "AI engine readiness + provider keys"
|
|
7
|
+
def status
|
|
8
|
+
RogIQ.load_rails!
|
|
9
|
+
payload = {
|
|
10
|
+
openai_key_present: ENV["OPENAI_API_KEY"].present?,
|
|
11
|
+
anthropic_key_present: ENV["ANTHROPIC_API_KEY"].present?,
|
|
12
|
+
ai_engine_ready: defined?(AIEngine) && AIEngine.ready?,
|
|
13
|
+
available_providers: (defined?(AIEngine) ? AIEngine.available_providers : [])
|
|
14
|
+
}
|
|
15
|
+
if defined?(Rails.configuration.ai_engine)
|
|
16
|
+
cfg = Rails.configuration.ai_engine
|
|
17
|
+
payload[:openai_enabled] = cfg.try(:openai_enabled)
|
|
18
|
+
payload[:anthropic_enabled] = cfg.try(:anthropic_enabled)
|
|
19
|
+
end
|
|
20
|
+
fmt.output(payload)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
desc "cost", "Roll up AI usage cost from ai_usage_daily"
|
|
24
|
+
method_option :days, type: :numeric, default: 30, desc: "Lookback window (days)"
|
|
25
|
+
method_option :account, type: :string, desc: "Limit to account id or slug"
|
|
26
|
+
def cost
|
|
27
|
+
RogIQ.load_rails!
|
|
28
|
+
start_date = options[:days].to_i.days.ago.to_date
|
|
29
|
+
scope = ::AIUsageDaily.where("usage_date >= ?", start_date)
|
|
30
|
+
if options[:account].present?
|
|
31
|
+
acc = RogIQ::Helpers.resolve_account(options[:account])
|
|
32
|
+
unless acc
|
|
33
|
+
fmt.error_msg("Account not found")
|
|
34
|
+
exit 1
|
|
35
|
+
end
|
|
36
|
+
scope = scope.where(account_id: acc.id)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
rows = scope.group(:provider, :model).sum(:total_cost).sort_by { |_, v| -v.to_f }.first(50)
|
|
40
|
+
fmt.output(headers: %w[provider model total_cost], rows: rows.map { |(p, m), t| [p, m, t.to_f.round(6)] })
|
|
41
|
+
total = scope.sum(:total_cost)
|
|
42
|
+
fmt.say("Total (period): #{total.to_f.round(6)}") unless options[:quiet]
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc "sync", "Enqueue AiModelSyncJob"
|
|
46
|
+
def sync
|
|
47
|
+
RogIQ.load_rails!
|
|
48
|
+
::AiModelSyncJob.perform_later
|
|
49
|
+
fmt.success("AiModelSyncJob enqueued.")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
desc "health_check", "Enqueue AiModelHealthCheckJob"
|
|
53
|
+
map "health-check" => :health_check
|
|
54
|
+
def health_check
|
|
55
|
+
RogIQ.load_rails!
|
|
56
|
+
::AiModelHealthCheckJob.perform_later
|
|
57
|
+
fmt.success("AiModelHealthCheckJob enqueued.")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
desc "models", "List AIModelConfig rows"
|
|
61
|
+
method_option :limit, type: :numeric, default: 100
|
|
62
|
+
def models
|
|
63
|
+
RogIQ.load_rails!
|
|
64
|
+
rows = ::AIModelConfig.order(:sort_order, :display_name).limit(options[:limit]).map do |m|
|
|
65
|
+
[m.model_key, m.provider, m.display_name, m.enabled, m.available?]
|
|
66
|
+
end
|
|
67
|
+
fmt.output(headers: %w[model_key provider display_name enabled available], rows: rows)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
require "thor"
|
|
8
|
+
|
|
9
|
+
require "rogiq/config_store"
|
|
10
|
+
require "rogiq/formatters"
|
|
11
|
+
require "rogiq/http_api"
|
|
12
|
+
|
|
13
|
+
module RogIQ
|
|
14
|
+
module Commands
|
|
15
|
+
class Auth < Thor
|
|
16
|
+
class_option :format,
|
|
17
|
+
aliases: "-f",
|
|
18
|
+
type: :string,
|
|
19
|
+
default: "table",
|
|
20
|
+
enum: %w[table json],
|
|
21
|
+
desc: "Output format"
|
|
22
|
+
|
|
23
|
+
desc "login", "Sign in and save a CLI API token to ~/.rogiq/config.yml"
|
|
24
|
+
method_option :api,
|
|
25
|
+
type: :string,
|
|
26
|
+
default: nil,
|
|
27
|
+
desc: "API base URL (default #{ENV.fetch('ROGIQ_API_URL', 'https://api.rogiq.ai')})"
|
|
28
|
+
def login
|
|
29
|
+
fmt = RogIQ::Formatters.new(format: options[:format])
|
|
30
|
+
base = options[:api].to_s.strip
|
|
31
|
+
base = ENV["ROGIQ_API_URL"].to_s.strip if base.empty?
|
|
32
|
+
base = ask("API base URL:", default: "https://api.rogiq.ai") if base.empty?
|
|
33
|
+
base = base.to_s.chomp("/")
|
|
34
|
+
email = ask("Email:")
|
|
35
|
+
password = ask("Password:", echo: false)
|
|
36
|
+
|
|
37
|
+
headers = { "Content-Type" => "application/json", "Accept" => "application/json" }
|
|
38
|
+
res, jwt = post_login(base, headers, email, password)
|
|
39
|
+
json = JSON.parse(res.body)
|
|
40
|
+
|
|
41
|
+
if json["mfa_required"]
|
|
42
|
+
mfa_token = json["mfa_token"]
|
|
43
|
+
code = ask("MFA code:")
|
|
44
|
+
remember = ask("Remember this device for 30 days? (y/N):").downcase.start_with?("y")
|
|
45
|
+
res, jwt = post_mfa_verify(base, headers, mfa_token, code, remember)
|
|
46
|
+
json = JSON.parse(res.body)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
unless jwt
|
|
50
|
+
fmt.error_msg(error_message(res))
|
|
51
|
+
exit 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
accounts = json["accounts"] || []
|
|
55
|
+
account_id = pick_account_id(fmt, accounts)
|
|
56
|
+
token_payload = mint_cli_token(base, jwt, account_id, headers)
|
|
57
|
+
plain = token_payload["token"]
|
|
58
|
+
unless plain
|
|
59
|
+
err = token_payload["error"] || token_payload["errors"]
|
|
60
|
+
fmt.error_msg("Could not create API token: #{err || token_payload.inspect}")
|
|
61
|
+
exit 1
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
RogIQ::ConfigStore.save(
|
|
65
|
+
"api_base_url" => base,
|
|
66
|
+
"token" => plain,
|
|
67
|
+
"account_id" => account_id,
|
|
68
|
+
"email" => email
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
fmt.success("Saved credentials to #{RogIQ::ConfigStore::PATH}")
|
|
72
|
+
out = token_payload.reject { |k, _| k.to_s == "token" }
|
|
73
|
+
out["token_saved"] = true
|
|
74
|
+
fmt.output(out)
|
|
75
|
+
rescue RogIQ::HttpApi::Error => e
|
|
76
|
+
fmt.error_msg(e.message)
|
|
77
|
+
exit 1
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
desc "logout", "Remove saved CLI credentials"
|
|
81
|
+
def logout
|
|
82
|
+
RogIQ::ConfigStore.clear!
|
|
83
|
+
puts "Removed #{RogIQ::ConfigStore::PATH}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
desc "whoami", "Print current user from /api/v1/me"
|
|
87
|
+
def whoami
|
|
88
|
+
fmt = RogIQ::Formatters.new(format: options[:format])
|
|
89
|
+
api = RogIQ::HttpApi.from_config!
|
|
90
|
+
data = api.get("/api/v1/me")
|
|
91
|
+
fmt.output(data)
|
|
92
|
+
rescue RogIQ::HttpApi::Error => e
|
|
93
|
+
fmt.error_msg(e.message)
|
|
94
|
+
exit 1
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
desc "use ACCOUNT_ID", "Set default X-Account-ID header for CLI requests"
|
|
98
|
+
def use(account_id)
|
|
99
|
+
RogIQ::ConfigStore.merge!("account_id" => account_id.to_s)
|
|
100
|
+
puts "Default account_id=#{account_id}"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def pick_account_id(fmt, accounts)
|
|
106
|
+
accounts = Array(accounts)
|
|
107
|
+
if accounts.empty?
|
|
108
|
+
return ask("Account ID (required for API token):").to_s.strip
|
|
109
|
+
end
|
|
110
|
+
return accounts.first["id"].to_s if accounts.size == 1
|
|
111
|
+
|
|
112
|
+
fmt.say("Accounts:")
|
|
113
|
+
accounts.each_with_index do |a, i|
|
|
114
|
+
fmt.say(" [#{i + 1}] #{a['name']} (id=#{a['id']})")
|
|
115
|
+
end
|
|
116
|
+
loop do
|
|
117
|
+
n = ask("Choose account number (1-#{accounts.size}):")
|
|
118
|
+
i = n.to_i
|
|
119
|
+
if i >= 1 && i <= accounts.size
|
|
120
|
+
return accounts[i - 1]["id"].to_s
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
fmt.warn_msg("Enter a number between 1 and #{accounts.size}")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def post_login(base, headers, email, password)
|
|
128
|
+
uri = URI.parse("#{base}/api/v1/auth/login")
|
|
129
|
+
req = Net::HTTP::Post.new(uri)
|
|
130
|
+
headers.each { |k, v| req[k] = v }
|
|
131
|
+
req.body = JSON.generate(user: { email: email, password: password })
|
|
132
|
+
response_for(uri, req)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def post_mfa_verify(base, headers, mfa_token, code, remember)
|
|
136
|
+
uri = URI.parse("#{base}/api/v1/auth/mfa/verify")
|
|
137
|
+
req = Net::HTTP::Post.new(uri)
|
|
138
|
+
headers.each { |k, v| req[k] = v }
|
|
139
|
+
req.body = JSON.generate(mfa_token: mfa_token, code: code, remember_me: remember)
|
|
140
|
+
response_for(uri, req)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def mint_cli_token(base, jwt, account_id, headers)
|
|
144
|
+
uri = URI.parse("#{base}/api/v1/api_tokens")
|
|
145
|
+
req = Net::HTTP::Post.new(uri)
|
|
146
|
+
headers.each { |k, v| req[k] = v }
|
|
147
|
+
req["Authorization"] = "Bearer #{jwt}"
|
|
148
|
+
aid = account_id.to_s.strip
|
|
149
|
+
req["X-Account-ID"] = aid unless aid.empty?
|
|
150
|
+
req.body = JSON.generate(api_token: { name: "RogIQ CLI", purpose: "cli" })
|
|
151
|
+
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
152
|
+
http.open_timeout = 15
|
|
153
|
+
http.read_timeout = 60
|
|
154
|
+
http.request(req)
|
|
155
|
+
end
|
|
156
|
+
body = res.body.to_s
|
|
157
|
+
parsed =
|
|
158
|
+
begin
|
|
159
|
+
body.strip.empty? ? {} : JSON.parse(body)
|
|
160
|
+
rescue JSON::ParserError
|
|
161
|
+
{ "error" => body[0, 300] }
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
return parsed.merge("error" => "HTTP #{res.code}") unless res.is_a?(Net::HTTPSuccess)
|
|
165
|
+
|
|
166
|
+
parsed
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def response_for(uri, req)
|
|
170
|
+
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
171
|
+
http.open_timeout = 15
|
|
172
|
+
http.read_timeout = 60
|
|
173
|
+
http.request(req)
|
|
174
|
+
end
|
|
175
|
+
jwt = extract_jwt(res)
|
|
176
|
+
[ res, jwt ]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def extract_jwt(res)
|
|
180
|
+
raw = res["Authorization"] || res["authorization"]
|
|
181
|
+
return nil if raw.nil? || raw.to_s.strip.empty?
|
|
182
|
+
|
|
183
|
+
raw.split(" ", 2).last
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def error_message(res)
|
|
187
|
+
json = JSON.parse(res.body)
|
|
188
|
+
[ json["error"], json["message"] ].map { |x| x.to_s.strip }.reject(&:empty?).first || "HTTP #{res.code}"
|
|
189
|
+
rescue JSON::ParserError
|
|
190
|
+
"HTTP #{res.code}: #{res.body.to_s[0, 200]}"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
|
|
5
|
+
module RogIQ
|
|
6
|
+
module Commands
|
|
7
|
+
class Base < Thor
|
|
8
|
+
class_option :format,
|
|
9
|
+
aliases: "-f",
|
|
10
|
+
type: :string,
|
|
11
|
+
default: "table",
|
|
12
|
+
enum: %w[table json],
|
|
13
|
+
desc: "Output format"
|
|
14
|
+
class_option :quiet,
|
|
15
|
+
type: :boolean,
|
|
16
|
+
default: false,
|
|
17
|
+
desc: "Suppress non-error output"
|
|
18
|
+
|
|
19
|
+
no_tasks do
|
|
20
|
+
def fmt
|
|
21
|
+
@fmt ||= RogIQ::Formatters.new(format: options[:format], quiet: options[:quiet])
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "billing_stripe"
|
|
4
|
+
|
|
5
|
+
module RogIQ
|
|
6
|
+
module Commands
|
|
7
|
+
class Billing < RogIQ::Commands::Base
|
|
8
|
+
desc "coins ACCOUNT", "Rogue coin wallet + recent transactions"
|
|
9
|
+
def coins(identifier)
|
|
10
|
+
RogIQ.load_rails!
|
|
11
|
+
account = RogIQ::Helpers.resolve_account(identifier)
|
|
12
|
+
unless account
|
|
13
|
+
fmt.error_msg("Account not found")
|
|
14
|
+
exit 1
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
w = account.rogue_coin_wallet
|
|
18
|
+
unless w
|
|
19
|
+
fmt.output(wallet: nil, message: "No RogueCoinWallet for account")
|
|
20
|
+
return
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
txs = w.transactions.order(created_at: :desc).limit(25).map do |t|
|
|
24
|
+
[t.created_at.iso8601, t.transaction_type, t.amount, t.description.to_s.truncate(60)]
|
|
25
|
+
end
|
|
26
|
+
fmt.output(
|
|
27
|
+
account_id: account.id,
|
|
28
|
+
balance: w.balance,
|
|
29
|
+
lifetime_purchased: w.lifetime_purchased,
|
|
30
|
+
lifetime_spent: w.lifetime_spent,
|
|
31
|
+
recent_transactions: txs
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
desc "overages", "Pending usage overages (all accounts)"
|
|
36
|
+
method_option :limit, type: :numeric, default: 50
|
|
37
|
+
def overages
|
|
38
|
+
RogIQ.load_rails!
|
|
39
|
+
rows = ::UsageOverage.status_pending.includes(:account).order(created_at: :desc).limit(options[:limit]).map do |o|
|
|
40
|
+
[
|
|
41
|
+
o.id,
|
|
42
|
+
o.account&.name,
|
|
43
|
+
o.metric_type,
|
|
44
|
+
o.total_charge.to_f,
|
|
45
|
+
o.created_at.iso8601
|
|
46
|
+
]
|
|
47
|
+
end
|
|
48
|
+
fmt.output(headers: %w[id account metric total_charge created_at], rows: rows)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
desc "stripe SUBCOMMAND", "Stripe catalog (sync)"
|
|
52
|
+
subcommand "stripe", RogIQ::Commands::BillingStripe
|
|
53
|
+
|
|
54
|
+
desc "summary", "Enqueue Billing::DailyBillingSummaryJob (Slack)"
|
|
55
|
+
method_option :date, type: :string, desc: "YYYY-MM-DD (default yesterday)"
|
|
56
|
+
def summary
|
|
57
|
+
RogIQ.load_rails!
|
|
58
|
+
date = options[:date].present? ? Date.parse(options[:date]) : nil
|
|
59
|
+
::Billing::DailyBillingSummaryJob.perform_later(date: date)
|
|
60
|
+
fmt.success("Billing::DailyBillingSummaryJob enqueued.")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
desc "invoices ACCOUNT", "Recent invoices"
|
|
64
|
+
method_option :limit, type: :numeric, default: 20
|
|
65
|
+
def invoices(identifier)
|
|
66
|
+
RogIQ.load_rails!
|
|
67
|
+
account = RogIQ::Helpers.resolve_account(identifier)
|
|
68
|
+
unless account
|
|
69
|
+
fmt.error_msg("Account not found")
|
|
70
|
+
exit 1
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
rows = account.invoices.order(created_at: :desc).limit(options[:limit]).map do |inv|
|
|
74
|
+
[
|
|
75
|
+
inv.id,
|
|
76
|
+
inv.invoice_number,
|
|
77
|
+
inv.status,
|
|
78
|
+
inv.total.to_f,
|
|
79
|
+
inv.striven_invoice_id,
|
|
80
|
+
inv.created_at.iso8601
|
|
81
|
+
]
|
|
82
|
+
end
|
|
83
|
+
fmt.output(
|
|
84
|
+
headers: %w[id invoice_number status total striven_invoice_id created_at],
|
|
85
|
+
rows: rows
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RogIQ
|
|
4
|
+
module Commands
|
|
5
|
+
class BillingStripe < RogIQ::Commands::Base
|
|
6
|
+
desc "sync", "Run rake stripe:ensure_subscription_prices"
|
|
7
|
+
def sync
|
|
8
|
+
RogIQ.load_rails!
|
|
9
|
+
Rails.application.load_tasks
|
|
10
|
+
Rake::Task["stripe:ensure_subscription_prices"].invoke
|
|
11
|
+
fmt.success("stripe:ensure_subscription_prices complete.")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|